[
  {
    "path": ".github/workflows/ci-rtk.yml",
    "content": "name: CI - RTK Query Build\n\non:\n  push:\n    branches:\n      - develop\n    paths:\n      - 'apps/rtk-query/**'\n  pull_request:\n    paths:\n      - 'apps/rtk-query/**'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - uses: pnpm/action-setup@v4\n        with:\n          run_install: false\n\n      - name: Install dependencies\n        working-directory: apps/rtk-query\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Build app\n        working-directory: apps/rtk-query\n        run: pnpm build\n"
  },
  {
    "path": ".github/workflows/ci-tanstack.yml",
    "content": "name: CI - TanStack Query Build\n\non:\n  push:\n    branches:\n      - develop\n    paths:\n      - 'apps/tanstack-query-zustand/**'\n  pull_request:\n    paths:\n      - 'apps/tanstack-query-zustand/**'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - uses: pnpm/action-setup@v4\n        with:\n          run_install: false\n\n      - name: Install dependencies\n        working-directory: apps/tanstack-query-zustand\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Build app\n        working-directory: apps/tanstack-query-zustand\n        run: pnpm build\n"
  },
  {
    "path": ".github/workflows/deploy-effector.yml",
    "content": "name: Deploy Effector App\n\non:\n  workflow_dispatch:\n\nconcurrency:\n  group: gh-pages-deploy\n  cancel-in-progress: false\n\npermissions:\n  contents: write\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - uses: pnpm/action-setup@v4\n        with:\n          run_install: false\n\n      - name: Install dependencies\n        working-directory: apps/react-effector-fsd\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Build app\n        working-directory: apps/react-effector-fsd\n        run: pnpm build\n\n      - name: Create SPA fallback\n        working-directory: apps/react-effector-fsd/dist\n        run: cp index.html 404.html\n\n      - name: Deploy to GitHub Pages\n        uses: JamesIves/github-pages-deploy-action@v4\n        with:\n          folder: apps/react-effector-fsd/dist\n          target-folder: effector\n          clean: false\n          branch: gh-pages\n"
  },
  {
    "path": ".github/workflows/deploy-reatom.yml",
    "content": "name: Deploy Reatom App\n\non:\n  workflow_dispatch:\n\nconcurrency:\n  group: gh-pages-deploy\n  cancel-in-progress: false\n\npermissions:\n  contents: write\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - uses: pnpm/action-setup@v4\n        with:\n          run_install: false\n\n      - name: Install dependencies\n        working-directory: apps/reatom\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Build app\n        working-directory: apps/reatom\n        run: pnpm build\n\n      - name: Create SPA fallback\n        working-directory: apps/reatom/dist\n        run: cp index.html 404.html\n\n      - name: Deploy to GitHub Pages\n        uses: JamesIves/github-pages-deploy-action@v4\n        with:\n          folder: apps/reatom/dist\n          target-folder: reatom\n          clean: false\n          branch: gh-pages\n"
  },
  {
    "path": ".github/workflows/deploy-root.yml",
    "content": "name: Deploy Root Landing Page\n\non:\n  workflow_dispatch:\n\nconcurrency:\n  group: gh-pages-deploy\n  cancel-in-progress: false\n\npermissions:\n  contents: write\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Prepare root files\n        run: |\n          mkdir -p temp-root\n          cp public/index.html temp-root/\n          cp public/404.html temp-root/\n          echo 'stg.musicfun.dev' > temp-root/CNAME\n          touch temp-root/.nojekyll\n\n      - name: Deploy to GitHub Pages\n        uses: JamesIves/github-pages-deploy-action@v4\n        with:\n          folder: temp-root\n          target-folder: .\n          clean: false\n          clean-exclude: |\n            tanstackquery/\n            rtkquery/\n            reatom/\n            effector/\n          branch: gh-pages\n"
  },
  {
    "path": ".github/workflows/deploy-rtk.yml",
    "content": "name: Deploy RTK Query App\n\non:\n  workflow_dispatch:\n\nconcurrency:\n  group: gh-pages-deploy\n  cancel-in-progress: false\n\npermissions:\n  contents: write\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - uses: pnpm/action-setup@v4\n        with:\n          run_install: false\n\n      - name: Install dependencies\n        working-directory: apps/rtk-query\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Build app\n        working-directory: apps/rtk-query\n        run: pnpm build\n\n      - name: Create SPA fallback\n        working-directory: apps/rtk-query/dist\n        run: cp index.html 404.html\n\n      - name: Deploy to GitHub Pages\n        uses: JamesIves/github-pages-deploy-action@v4\n        with:\n          folder: apps/rtk-query/dist\n          target-folder: rtkquery\n          clean: false\n          branch: gh-pages\n"
  },
  {
    "path": ".github/workflows/deploy-tanstack.yml",
    "content": "name: Deploy TanStack Query App\n\non:\n  workflow_dispatch:\n\nconcurrency:\n  group: gh-pages-deploy\n  cancel-in-progress: false\n\npermissions:\n  contents: write\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - uses: pnpm/action-setup@v4\n        with:\n          run_install: false\n\n      - name: Install dependencies\n        working-directory: apps/tanstack-query-zustand\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Build app\n        working-directory: apps/tanstack-query-zustand\n        run: pnpm build\n\n      - name: Create SPA fallback\n        working-directory: apps/tanstack-query-zustand/dist\n        run: cp index.html 404.html\n\n      - name: Deploy to GitHub Pages\n        uses: JamesIves/github-pages-deploy-action@v4\n        with:\n          folder: apps/tanstack-query-zustand/dist\n          target-folder: tanstackquery\n          clean: false\n          branch: gh-pages\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy MusicFun Apps to GitHub Pages\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: pages-deploy\n  cancel-in-progress: true\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - uses: pnpm/action-setup@v4\n        with:\n          run_install: false\n\n      # === Install ===\n\n      - name: Install deps for tanstack-query (folder renamed)\n        working-directory: apps/tanstack-query-zustand\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Install deps for rtk-query\n        working-directory: apps/rtk-query\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Install deps for reatom\n        working-directory: apps/reatom\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Install deps for effector\n        working-directory: apps/react-effector-fsd\n        run: pnpm install --no-frozen-lockfile\n\n      # === Build ===\n\n      - name: Build tanstack-query\n        working-directory: apps/tanstack-query-zustand\n        run: pnpm build\n\n      - name: Build rtk-query\n        working-directory: apps/rtk-query\n        run: pnpm build\n\n      - name: Build reatom\n        working-directory: apps/reatom\n        run: pnpm build\n\n      - name: Build effector\n        working-directory: apps/react-effector-fsd\n        run: pnpm build\n\n      # === Pages config ===\n\n      - uses: actions/configure-pages@v5\n        with:\n          enablement: true\n\n      # === SPA fallback ===\n\n      - name: SPA fallback for tanstack-query\n        working-directory: apps/tanstack-query-zustand/dist\n        run: cp index.html 404.html\n\n      - name: SPA fallback for rtk-query\n        working-directory: apps/rtk-query/dist\n        run: cp index.html 404.html\n\n      - name: SPA fallback for reatom\n        working-directory: apps/reatom/dist\n        run: cp index.html 404.html\n\n      - name: SPA fallback for effector\n        working-directory: apps/react-effector-fsd/dist\n        run: cp index.html 404.html\n\n      # === Prepare deploy folder ===\n\n      - name: Prepare deployment folder\n        run: |\n          mkdir -p dist/tanstackquery\n          mkdir -p dist/rtkquery\n          mkdir -p dist/reatom\n          mkdir -p dist/effector\n\n          cp -r apps/tanstack-query-zustand/dist/* dist/tanstackquery/\n          cp -r apps/rtk-query/dist/* dist/rtkquery/\n          cp -r apps/reatom/dist/* dist/reatom/\n          cp -r apps/react-effector-fsd/dist/* dist/effector/\n\n          touch dist/.nojekyll\n\n      # === Root pages ===\n\n      - name: Copy root index.html\n        run: cp public/index.html dist/index.html\n\n      - name: Copy root 404.html\n        run: cp public/404.html dist/404.html\n\n      - name: Add CNAME\n        run: echo 'stg.musicfun.dev' > dist/CNAME\n\n      - uses: actions/upload-pages-artifact@v3\n        with:\n          path: dist\n\n      - id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.cursor\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n.cursorignore\n\n# env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "# ─── Auto-increment VITE_VERSION for rtk-query ──────────────────────────────\n#\n# If any staged file inside apps/rtk-query/ changed (excluding .env itself),\n# bump VITE_VERSION in .env by 1 and stage the updated file.\n# ─────────────────────────────────────────────────────────────────────────────\n\nENV_FILE=\"apps/rtk-query/.env\"\n\nif git diff --cached --name-only -- 'apps/rtk-query/' | grep -qv '\\.env$'; then\n  CURRENT=$(grep \"^VITE_VERSION=\" \"$ENV_FILE\" | cut -d'=' -f2)\n  NEW=$((CURRENT + 1))\n  sed -i '' \"s/^VITE_VERSION=.*/VITE_VERSION=$NEW/\" \"$ENV_FILE\"\n  git add \"$ENV_FILE\"\n  echo \"✔ Bumped rtk-query VITE_VERSION: $CURRENT → $NEW\"\nfi\n\npnpm exec lint-staged\n"
  },
  {
    "path": ".husky/pre-push",
    "content": "#!/usr/bin/env sh\n\n# This script runs automatically before every \"git push\".\n# If any build fails, the push is cancelled — broken code won't reach the remote.\n\n# ─── Step 1: Figure out which files were changed ───────────────────────────────\n#\n# @{push} is a git shorthand for \"the branch on the remote that we're pushing to\".\n# \"git diff --name-only @{push}..\" compares the remote state with our local HEAD\n# and outputs just the file paths (one per line) that differ.\n#\n# 2>/dev/null suppresses errors — for example, if the branch has no remote tracking\n# branch yet (first push), this command would fail.\n#\n# If it fails, we fall back to \"git diff --name-only HEAD~1\" which compares\n# the latest commit with the one before it (so at least we check the last commit).\n# ───────────────────────────────────────────────────────────────────────────────\nCHANGED=$(git diff --name-only @{push}.. 2>/dev/null || git diff --name-only HEAD~1)\n\n# ─── Step 2: Initialize flags for each project ────────────────────────────────\n#\n# We use two boolean flags to track whether each project needs to be built.\n# They start as \"false\" and will be set to \"true\" if relevant files were changed.\n# ───────────────────────────────────────────────────────────────────────────────\nRTK=false\nTANSTACK=false\n\n# ─── Step 3: Check which projects have changed files ──────────────────────────\n#\n# We pipe the list of changed files into grep.\n#   -q flag means \"quiet\" — grep won't print anything, it just sets the exit code:\n#     exit 0 (success) if a match is found, exit 1 (failure) if not.\n#\n# \"^apps/rtk-query/\" means \"line starts with apps/rtk-query/\" — so any file\n# inside that folder will match.\n#\n# \"&&\" means \"if the previous command succeeded (match found), run the next command\".\n# So if any changed file is inside apps/rtk-query/, we set RTK=true.\n# Same logic for tanstack-query-zustand.\n# ───────────────────────────────────────────────────────────────────────────────\necho \"$CHANGED\" | grep -q \"^apps/rtk-query/\" && RTK=true\necho \"$CHANGED\" | grep -q \"^apps/tanstack-query-zustand/\" && TANSTACK=true\n\n# ─── Step 4: Type-check only the projects that were changed ───────────────────\n#\n# We run only \"tsc -b\" (TypeScript compiler in build mode) instead of a full\n# \"pnpm build\" (which would also run Vite bundling). tsc catches type errors,\n# missing imports, wrong props, etc. Vite build is redundant here — it doesn't\n# check types, just bundles JS. Full build is done in CI instead.\n#\n# \"|| exit 1\" means: if tsc fails (returns non-zero exit code),\n# immediately exit this script with code 1. A non-zero exit from a pre-push\n# hook tells git to ABORT the push. This prevents pushing broken code.\n# ───────────────────────────────────────────────────────────────────────────────\n\nif [ \"$RTK\" = true ]; then\n  echo \"▶ Type-checking rtk-query...\"\n  pnpm --prefix apps/rtk-query exec tsc -b || exit 1\nfi\n\nif [ \"$TANSTACK\" = true ]; then\n  echo \"▶ Type-checking tanstack-query-zustand...\"\n  pnpm --prefix apps/tanstack-query-zustand exec tsc -b || exit 1\nfi\n\n# If we reach this point, either:\n#   - No projects had changes (nothing to check), or\n#   - All type checks passed.\n# In both cases the script exits with code 0 (success) and git proceeds with the push.\n"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules\ndist\nbuild\n.next\n.nuxt\ncoverage\n*.log\npnpm-lock.yaml\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"es5\",\n  \"bracketSameLine\": true,\n  \"arrowParens\": \"always\",\n  \"endOfLine\": \"auto\",\n  \"printWidth\": 100,\n  \"semi\": false\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "> [!NOTE]\n> We prefer English language for all communication.\n\n## Creating an issue\n\nBefore creating an issue please ensure that the problem is not [already reported](https://github.com/it-incubator/musicfun-react-all-stacks/issues).\n\nIf you want to report a bug, create a reproduction using StackBlitz or CodeSandbox. If you want to request a feature, add motivation section and some usage examples.\n\n## Sending a Pull Request\n\n1. fork and clone the repository\n   > [!NOTE]\n   > You can just clone the repository if you are a collaborator\n2. create a development branch from `main`\n3. run the following command in the project root (this will install dependencies for all apps and packages)\n   > [!NOTE]\n   > It is recommended to create a branch from the issue\n4. [make changes](#coding-guide) and [commit them](#commit-messages)\n5. upload feature branch and create a [Pull Request](https://github.com/it-incubator/musicfun-react-all-stacks/compare) to merge changes to `main`\n6. link your PR to the issue using a [closing keyword](https://help.github.com/en/articles/closing-issues-using-keywords) or provide changes description with motivation and explanation in the comment (example: `fix #74`)\n7. wait until a team member responds\n\n## Coding guide\n\n<!-- - always use `@ts-expect-error` instead of `@ts-ignore` -->\n\n- use `// @ts-ignore` if you not sure why error appears or you think it could be better, use `// @ts-expect-error` if you sure that error is a mistake <!-- ??? -->\n\n## Commit messages\n\nCommit messages should follow the [Conventional Commits](https://conventionalcommits.org) specification:\n\n```\n<type>[optional scope]: <description>\n```\n\n### Allowed `<type>`\n\n- `chore`: any repository maintainance changes\n- `feat`: code change that adds a new feature\n- `fix`: bug fix\n- `perf`: code change that improves performance\n- `refactor`: code change that is neither a feature addition nor a bug fix nor a performance improvement\n- `docs`: documentation only changes\n- `ci`: a change made to CI configurations and scripts\n- `style`: cosmetic code change\n- `test`: change that only adds or corrects tests\n- `revert`: change that reverts previous commits\n\n### Allowed `<scope>`\n\nPackage directory name. Eg: `/packages/effects` is scoped as `effects`.\n\n### `<description>` rules\n\n- should be written in English\n- should be in imperative mood (like `change` instead `changed` or `changes`)\n- should not be capitalized\n- should not have period (`.`) at the end\n\n### Commit message examples\n\n```\ndocs: fix typo in npm-react\nfix(core): add check for atoms with equal ids\n```\n"
  },
  {
    "path": "FRONTEND_API_CHANGES.md",
    "content": "# Frontend API Changes - January 27-29, 2026\n\nThis document summarizes the API changes from the last 5 commits that require frontend updates.\n\n---\n\n## Table of Contents\n\n1. [New Endpoints](#new-endpoints)\n2. [Response Format Changes](#response-format-changes)\n3. [Request Payload Changes (Breaking)](#request-payload-changes-breaking)\n\n---\n\n## New Endpoints\n\n### 1. Get Playlists Count\n\n**Endpoint:** `GET /playlists/count/:userId`\n\nReturns the total number of playlists for a specific user.\n\n**Response:**\n\n```json\n{\n  \"count\": 5\n}\n```\n\n**TypeScript Interface:**\n\n```typescript\ninterface GetPlaylistsCountOutput {\n  count: number\n}\n```\n\n---\n\n### 2. Get Tracks Count\n\n**Endpoint:** `GET /playlists/tracks/count/:userId`\n\nReturns the total number of **published** tracks for a specific user.\n\n**Response:**\n\n```json\n{\n  \"count\": 12\n}\n```\n\n**TypeScript Interface:**\n\n```typescript\ninterface GetTracksCountOutput {\n  count: number\n}\n```\n\n> **Note:** Only published tracks are counted. Draft/unpublished tracks are excluded.\n\n---\n\n## Response Format Changes\n\n### 1. Playlists List - `description` Field Removed\n\n**Endpoint:** `GET /playlists`\n\nThe `description` field has been **removed** from the playlist list response.\n\n**Before:**\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"...\",\n      \"type\": \"playlists\",\n      \"attributes\": {\n        \"title\": \"My Playlist\",\n        \"description\": \"Playlist description\",  // ❌ REMOVED\n        \"tracksCount\": 10,\n        ...\n      }\n    }\n  ]\n}\n```\n\n**After:**\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"...\",\n      \"type\": \"playlists\",\n      \"attributes\": {\n        \"title\": \"My Playlist\",\n        \"tracksCount\": 10,\n        ...\n      }\n    }\n  ]\n}\n```\n\n> **Note:** The `description` field is still available when fetching a **single playlist** via `GET /playlists/:playlistId`.\n\n---\n\n### 2. Playlists - New `tracksCount` Field\n\n**Endpoints:**\n\n- `GET /playlists` (list)\n- `GET /playlists/:playlistId` (single)\n\nA new `tracksCount` field has been added to playlist responses.\n\n**Response:**\n\n```json\n{\n  \"data\": {\n    \"id\": \"...\",\n    \"type\": \"playlists\",\n    \"attributes\": {\n      \"title\": \"My Playlist\",\n      \"tracksCount\": 10,  // ✅ NEW FIELD\n      ...\n    }\n  }\n}\n```\n\n**TypeScript Update:**\n\n```typescript\ninterface PlaylistAttributes {\n  // ... existing fields\n  tracksCount: number // NEW\n}\n```\n\n---\n\n### 3. Tags - JSON:API Format\n\n**Endpoints:**\n\n- `POST /tags` (create)\n- `GET /tags/search` (search)\n\nTags endpoints now return JSON:API formatted responses.\n\n**Before (Create):**\n\n```json\n{\n  \"id\": \"uuid\",\n  \"name\": \"Rock\"\n}\n```\n\n**After (Create):**\n\n```json\n{\n  \"data\": {\n    \"id\": \"uuid\",\n    \"type\": \"tags\",\n    \"attributes\": {\n      \"name\": \"Rock\"\n    }\n  }\n}\n```\n\n**Before (Search):**\n\n```json\n[\n  { \"id\": \"uuid1\", \"name\": \"Rock\" },\n  { \"id\": \"uuid2\", \"name\": \"Pop\" }\n]\n```\n\n**After (Search):**\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"uuid1\",\n      \"type\": \"tags\",\n      \"attributes\": { \"name\": \"Rock\" }\n    },\n    {\n      \"id\": \"uuid2\",\n      \"type\": \"tags\",\n      \"attributes\": { \"name\": \"Pop\" }\n    }\n  ]\n}\n```\n\n**TypeScript Interfaces:**\n\n```typescript\ninterface TagAttributes {\n  name: string\n}\n\ninterface TagResource {\n  id: string\n  type: 'tags'\n  attributes: TagAttributes\n}\n\ninterface GetTagOutput {\n  data: TagResource\n}\n\ninterface GetTagsOutput {\n  data: TagResource[]\n}\n```\n\n---\n\n## Request Payload Changes (Breaking)\n\nAll create/update endpoints now use **JSON:API format** for request bodies.\n\n### 1. Create Tag\n\n**Endpoint:** `POST /tags`\n\n**Before:**\n\n```json\n{\n  \"name\": \"Rock\"\n}\n```\n\n**After:**\n\n```json\n{\n  \"data\": {\n    \"type\": \"tags\",\n    \"attributes\": {\n      \"name\": \"Rock\"\n    }\n  }\n}\n```\n\n---\n\n### 2. Create Artist\n\n**Endpoint:** `POST /artists`\n\n**Before:**\n\n```json\n{\n  \"name\": \"Artist Name\"\n}\n```\n\n**After:**\n\n```json\n{\n  \"data\": {\n    \"type\": \"artists\",\n    \"attributes\": {\n      \"name\": \"Artist Name\"\n    }\n  }\n}\n```\n\n---\n\n### 3. Create Playlist\n\n**Endpoint:** `POST /playlists`\n\n**Before:**\n\n```json\n{\n  \"title\": \"My Playlist\",\n  \"description\": \"Description\"\n}\n```\n\n**After:**\n\n```json\n{\n  \"data\": {\n    \"type\": \"playlists\",\n    \"attributes\": {\n      \"title\": \"My Playlist\",\n      \"description\": \"Description\"\n    }\n  }\n}\n```\n\n---\n\n### 4. Update Playlist\n\n**Endpoint:** `PUT /playlists/:id`\n\n**Before:**\n\n```json\n{\n  \"title\": \"Updated Title\",\n  \"description\": \"Updated description\"\n}\n```\n\n**After:**\n\n```json\n{\n  \"data\": {\n    \"type\": \"playlists\",\n    \"attributes\": {\n      \"title\": \"Updated Title\",\n      \"description\": \"Updated description\"\n    }\n  }\n}\n```\n\n---\n\n### 5. Upload Track\n\n**Endpoint:** `POST /tracks` (multipart/form-data)\n\n**Before:**\n\n```\ntitle: \"Track Title\"\nartists: [\"artist-id-1\", \"artist-id-2\"]\ntags: [\"tag-id-1\"]\n```\n\n**After:**\n\n```\ndata[type]: \"tracks\"\ndata[attributes][title]: \"Track Title\"\ndata[attributes][artists]: [\"artist-id-1\", \"artist-id-2\"]\ndata[attributes][tags]: [\"tag-id-1\"]\n```\n\n---\n\n### 6. Update Track\n\n**Endpoint:** `PATCH /tracks/:id`\n\n**Before:**\n\n```json\n{\n  \"title\": \"Updated Title\",\n  \"artists\": [\"artist-id\"],\n  \"tags\": [\"tag-id\"]\n}\n```\n\n**After:**\n\n```json\n{\n  \"data\": {\n    \"type\": \"tracks\",\n    \"attributes\": {\n      \"title\": \"Updated Title\",\n      \"artists\": [\"artist-id\"],\n      \"tags\": [\"tag-id\"]\n    }\n  }\n}\n```\n\n---\n\n### 7. Add Track to Playlist\n\n**Endpoint:** `POST /playlists/:id/tracks`\n\n**Before:**\n\n```json\n{\n  \"trackId\": \"track-uuid\"\n}\n```\n\n**After:**\n\n```json\n{\n  \"data\": {\n    \"type\": \"playlist-tracks\",\n    \"attributes\": {\n      \"trackId\": \"track-uuid\"\n    }\n  }\n}\n```\n\n---\n\n## Summary of Breaking Changes\n\n| Category        | Change                                              | Impact                                              |\n| --------------- | --------------------------------------------------- | --------------------------------------------------- |\n| Request Format  | All create/update payloads now use JSON:API wrapper | **HIGH** - All POST/PUT/PATCH requests need updates |\n| Response Format | Tags endpoints now return JSON:API format           | **MEDIUM** - Update tag parsing logic               |\n| Response Format | Playlist list no longer includes `description`      | **LOW** - Remove usage or fetch single playlist     |\n| New Field       | `tracksCount` added to playlist responses           | **LOW** - Can be used for UI display                |\n| New Endpoints   | `/playlists/count/:userId`                          | **NONE** - New feature                              |\n| New Endpoints   | `/playlists/tracks/count/:userId`                   | **NONE** - New feature                              |\n\n---\n\n## Migration Checklist\n\n- [ ] Update all API request payloads to JSON:API format\n- [ ] Update tag response parsing (access via `response.data` / `response.data.attributes`)\n- [ ] Remove reliance on `description` field in playlist lists\n- [ ] Add `tracksCount` to playlist TypeScript interfaces\n- [ ] (Optional) Implement new count endpoints for user statistics\n\n---\n\n## TypeScript Helper Types\n\n```typescript\n// Generic JSON:API Request Wrapper\ninterface JsonApiRequest<T extends string, A> {\n  data: {\n    type: T\n    attributes: A\n  }\n}\n\n// Example usage:\ntype CreateTagRequest = JsonApiRequest<'tags', { name: string }>\ntype CreatePlaylistRequest = JsonApiRequest<\n  'playlists',\n  {\n    title: string\n    description: string | null\n  }\n>\ntype CreateArtistRequest = JsonApiRequest<'artists', { name: string }>\ntype UpdateTrackRequest = JsonApiRequest<\n  'tracks',\n  {\n    title?: string\n    artists?: string[]\n    tags?: string[]\n  }\n>\n```\n"
  },
  {
    "path": "README.md",
    "content": "[Figma](https://www.figma.com/design/AxTPd4AS8oAgdEF4dDgLis/MusicFun?node-id=9-353&p=f&t=I0svXbRE8kPWOUFB-0) • [ApiHub](https://apihub.it-incubator.io/en) • [Swagger](https://musicfun.it-incubator.app/api)\n\n# 🚀 Project Launch\n\nInformation on launching projects can be found in the `README.md` of each individual repository.\n\n## Actual projects\n\n- `youtube/rtk-query` - youtube lessons: rtk-query\n\n- `youtube/tanstack-query-router-fsd` - youtube lessons: tanstack-query\n\n- `apps/musicfun-ui-vanilla` - full project html/css/storybook vanilla without ui libraries\n\n- `apps/musicfun-tanstack-query` - full project with tanstack query\n\n- `apps/musicfun-rtk-query` - full project with rtk-query\n\n## ❌ Project Launch with SDK (Currently Unsupported)\n\n### 1. Installing Dependencies\n\nRun the following command in the project root (this will install dependencies for all apps and packages):\n\n```bash\npnpm i\n```\n\n### 2. SDK build\n\nThen build `musicfun-api-sdk`\n\n```bash\npnpm build:sdk\n```\n\n️⚠️ Note: Some scripts may not be cross-platform compatible:\n\n```json\n\"scripts\": {\n\"clean\": \"rm -rf dist\",\n\"build\": \"pnpm run clean && tsc\"\n}\n```\n\nIf so, try a simpler alternative command:\n\n```bash\npnpm build:sdk:simple\n```\n\n### 3. Starting the Project\n\n- 🎶musicfun на **tanstack**\n\n```bash\n   pnpm start:musicfun-tanstack\n```\n\n- 🎶musicfun на **rtk-query**\n\n```bash\n    pnpm start:musicfun-rtk\n```\n\n- 🎶musicfun на **nextjs**\n\n```bash\n     pnpm start:musicfun-nextjs\n```\n\n## ✅ Рекомендованные форматы нейминга файлов в React/TypeScript проектах\n\n| Category              | Recommended Format | Example                               |\n| --------------------- | ------------------ | ------------------------------------- |\n| **Components**        | `PascalCase`       | `UserCard.tsx`                        |\n| **Hooks**             | `camelCase`        | `useAuth.ts`                          |\n| **Utilities (utils)** | `kebab-case`       | `format-date.ts`, `validate-email.ts` |\n| **Redux Slice/State** | `kebab-case`       | `auth-slice.ts`, `user-slice.ts`      |\n| **API files**         | `kebab-case`       | `playlists-api.ts`, `auth-api.ts`     |\n| **Types/Interfaces**  | `kebab-case`       | `user.types.ts`, `auth.types.ts`      |\n| **Services**          | `kebab-case`       | `auth-service.ts`, `user-service.ts`  |\n| **Mocks (mock data)** | `kebab-case`       | `user-mocks.ts`, `playlist-mocks.ts`  |\n\n## Contributing\n\nPlease refer to our [Contributing guide](CONTRIBUTING.md) to learn about our development process, how to propose bugfixes\n\n### Happy hacking 🚀 🚀🚀\n"
  },
  {
    "path": "apps/nextjs/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "apps/nextjs/README.md",
    "content": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n"
  },
  {
    "path": "apps/nextjs/eslint.config.mjs",
    "content": "import { FlatCompat } from '@eslint/eslintrc'\nimport { dirname } from 'path'\nimport { fileURLToPath } from 'url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n})\n\nconst eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')]\n\nexport default eslintConfig\n"
  },
  {
    "path": "apps/nextjs/next.config.ts",
    "content": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n}\n\nexport default nextConfig\n"
  },
  {
    "path": "apps/nextjs/package.json",
    "content": "{\n  \"name\": \"musicfun-nextjs\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"NODE_OPTIONS='--inspect' next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"next\": \"15.3.3\",\n    \"@it-incubator/musicfun-api-sdk\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\"\n  }\n}\n"
  },
  {
    "path": "apps/nextjs/src/app/actions/auth/logout.action.tsx",
    "content": "'use server'\nimport { cookies } from 'next/headers'\n\nexport async function logout() {\n  const cookieStore = await cookies()\n  // удаляем куки, задав maxAge: 0\n  cookieStore.set('access-token', '', { httpOnly: true, maxAge: 0, path: '/' })\n  cookieStore.set('refresh-token', '', { httpOnly: true, maxAge: 0, path: '/' })\n}\n"
  },
  {
    "path": "apps/nextjs/src/app/api/oauth/callback/route.ts",
    "content": "import { cookies } from 'next/headers'\nimport { NextResponse } from 'next/server'\n\nimport { authApi } from '@/shared/api/auth-api'\nimport { redirectAfterOauthUri } from '@/shared/api/base'\nimport { createAccessTokenCookie, createRefreshTokenCookie } from '@/shared/utils/cookieHelpers'\n\nexport async function GET(request: Request) {\n  const { searchParams } = new URL(request.url)\n  const code = searchParams.get('code') as string\n\n  const tokens = await authApi.login({\n    code,\n    redirectUri: redirectAfterOauthUri,\n    accessTokenTTL: '1d',\n    rememberMe: true,\n  })\n\n  const cookieStore = await cookies()\n\n  cookieStore.set(createRefreshTokenCookie(tokens.refreshToken))\n  cookieStore.set(createAccessTokenCookie(tokens.accessToken))\n\n  return NextResponse.redirect(new URL('/', request.url), 307)\n}\n"
  },
  {
    "path": "apps/nextjs/src/app/globals.css",
    "content": ":root {\n  --background: #ffffff;\n  --foreground: #171717;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --background: #0a0a0a;\n    --foreground: #ededed;\n  }\n}\n\nhtml,\nbody {\n  max-width: 100vw;\n  overflow-x: hidden;\n}\n\nbody {\n  color: var(--foreground);\n  background: var(--background);\n  font-family: Arial, Helvetica, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n* {\n  box-sizing: border-box;\n  padding: 0;\n  margin: 0;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n}\n"
  },
  {
    "path": "apps/nextjs/src/app/layout.tsx",
    "content": "import './globals.css'\n\nimport type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\n\nconst geistSans = Geist({\n  variable: '--font-geist-sans',\n  subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n  variable: '--font-geist-mono',\n  subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n  title: 'Create Next App',\n  description: 'Generated by create next app',\n}\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode\n}>) {\n  return (\n    <html lang=\"en\">\n      <body className={`${geistSans.variable} ${geistMono.variable}`}>{children}</body>\n    </html>\n  )\n}\n"
  },
  {
    "path": "apps/nextjs/src/app/page.module.css",
    "content": ".page {\n  --gray-rgb: 0, 0, 0;\n  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);\n  --gray-alpha-100: rgba(var(--gray-rgb), 0.05);\n\n  --button-primary-hover: #383838;\n  --button-secondary-hover: #f2f2f2;\n\n  display: grid;\n  grid-template-rows: 20px 1fr 20px;\n  align-items: center;\n  justify-items: center;\n  min-height: 100svh;\n  padding: 80px;\n  gap: 64px;\n  font-family: var(--font-geist-sans);\n}\n\n@media (prefers-color-scheme: dark) {\n  .page {\n    --gray-rgb: 255, 255, 255;\n    --gray-alpha-200: rgba(var(--gray-rgb), 0.145);\n    --gray-alpha-100: rgba(var(--gray-rgb), 0.06);\n\n    --button-primary-hover: #ccc;\n    --button-secondary-hover: #1a1a1a;\n  }\n}\n\n.main {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n  grid-row-start: 2;\n}\n\n.main ol {\n  font-family: var(--font-geist-mono);\n  padding-left: 0;\n  margin: 0;\n  font-size: 14px;\n  line-height: 24px;\n  letter-spacing: -0.01em;\n  list-style-position: inside;\n}\n\n.main li:not(:last-of-type) {\n  margin-bottom: 8px;\n}\n\n.main code {\n  font-family: inherit;\n  background: var(--gray-alpha-100);\n  padding: 2px 4px;\n  border-radius: 4px;\n  font-weight: 600;\n}\n\n.ctas {\n  display: flex;\n  gap: 16px;\n}\n\n.ctas a {\n  appearance: none;\n  border-radius: 128px;\n  height: 48px;\n  padding: 0 20px;\n  border: none;\n  border: 1px solid transparent;\n  transition:\n    background 0.2s,\n    color 0.2s,\n    border-color 0.2s;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 16px;\n  line-height: 20px;\n  font-weight: 500;\n}\n\na.primary {\n  background: var(--foreground);\n  color: var(--background);\n  gap: 8px;\n}\n\na.secondary {\n  border-color: var(--gray-alpha-200);\n  min-width: 158px;\n}\n\n.footer {\n  grid-row-start: 3;\n  display: flex;\n  gap: 24px;\n}\n\n.footer a {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.footer img {\n  flex-shrink: 0;\n}\n\n/* Enable hover only on non-touch devices */\n@media (hover: hover) and (pointer: fine) {\n  a.primary:hover {\n    background: var(--button-primary-hover);\n    border-color: transparent;\n  }\n\n  a.secondary:hover {\n    background: var(--button-secondary-hover);\n    border-color: transparent;\n  }\n\n  .footer a:hover {\n    text-decoration: underline;\n    text-underline-offset: 4px;\n  }\n}\n\n@media (max-width: 600px) {\n  .page {\n    padding: 32px;\n    padding-bottom: 80px;\n  }\n\n  .main {\n    align-items: center;\n  }\n\n  .main ol {\n    text-align: center;\n  }\n\n  .ctas {\n    flex-direction: column;\n  }\n\n  .ctas a {\n    font-size: 14px;\n    height: 40px;\n    padding: 0 16px;\n  }\n\n  a.secondary {\n    min-width: auto;\n  }\n\n  .footer {\n    flex-wrap: wrap;\n    align-items: center;\n    justify-content: center;\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .logo {\n    filter: invert();\n  }\n}\n"
  },
  {
    "path": "apps/nextjs/src/app/page.tsx",
    "content": "import { UserBlock } from '@/features/auth/ui/UserBlock'\nimport { tracksApi } from '@/shared/api/tracks/tracksApi'\n\nimport styles from './page.module.css'\n\nexport default async function Home() {\n  const tracks = await tracksApi.fetchTracks({ pageNumber: 1, pageSize: 5 })\n\n  return (\n    <div className={styles.page}>\n      <header>\n        <UserBlock />\n      </header>\n      <h2>Tracks:</h2>\n      {tracks.data.map((track) => (\n        <li key={track.id}>{track.attributes.title}</li>\n      ))}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/nextjs/src/app/profile/page.tsx",
    "content": "import { cookies } from 'next/headers'\n\nimport { authApi } from '@/shared/api/auth-api'\nimport { MeResponseResponse } from '@/shared/api/authApi.types'\nimport { redirectAfterOauthUri } from '@/shared/api/base'\n\nexport default async function ProfilePage() {\n  let meData: MeResponseResponse | null = null\n  try {\n    meData = await authApi.getMe()\n  } catch (error) {}\n\n  return meData ? (\n    <div>\n      login: {meData.login}, userId: {meData.userId}\n    </div>\n  ) : (\n    <div>Login</div>\n  )\n}\n"
  },
  {
    "path": "apps/nextjs/src/app/redirect/page.tsx",
    "content": "import { cookies } from 'next/headers'\nimport { redirect } from 'next/navigation'\n\nimport { authApi } from '@/shared/api/auth-api'\nimport { MeResponseResponse } from '@/shared/api/authApi.types'\nimport { redirectAfterOauthUri } from '@/shared/api/base'\n\nexport default async function ProfilePage() {\n  let meData: MeResponseResponse | null = null\n  try {\n    meData = await authApi.getMe()\n  } catch (error) {}\n\n  if (meData) {\n    redirect('/profile')\n  } else {\n    redirect('/')\n  }\n}\n"
  },
  {
    "path": "apps/nextjs/src/features/auth/ui/Login/Login.tsx",
    "content": "import { authApi } from '@/shared/api/auth-api'\nimport { redirectAfterOauthUri } from '@/shared/api/base'\n\nexport const Login = () => {\n  return <a href={authApi.oauthUrl(redirectAfterOauthUri)}>Login via APIHUB</a>\n}\n"
  },
  {
    "path": "apps/nextjs/src/features/auth/ui/Logout/Logout.tsx",
    "content": "'use client'\nimport { useRouter } from 'next/navigation'\n\nimport { logout } from '@/app/actions/auth/logout.action'\n\nexport const Logout = () => {\n  const router = useRouter()\n  const logoutHandler = async () => {\n    await logout()\n    router.push('/')\n  }\n\n  return <button onClick={logoutHandler}>logout</button>\n}\n"
  },
  {
    "path": "apps/nextjs/src/features/auth/ui/MeInfo/MeInfo.tsx",
    "content": "import { Logout } from '@/features/auth/ui/Logout/Logout'\nimport { authApi } from '@/shared/api/auth-api'\n\nexport const MeInfo = async () => {\n  const meData = await authApi.getMe()\n\n  return (\n    <div>\n      userLogin: {meData.login}\n      <Logout />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/nextjs/src/features/auth/ui/UserBlock.tsx",
    "content": "import { Login } from '@/features/auth/ui/Login/Login'\nimport { MeInfo } from '@/features/auth/ui/MeInfo/MeInfo'\nimport { authApi } from '@/shared/api/auth-api'\nimport { MeResponseResponse } from '@/shared/api/authApi.types'\n\nexport const UserBlock = async () => {\n  let meData: MeResponseResponse | null = null\n  try {\n    meData = await authApi.getMe()\n  } catch (error) {}\n\n  return (\n    <>\n      {!meData && <Login />}\n      {meData && <MeInfo />}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/nextjs/src/middleware.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\n\nimport { reauthMiddleware } from '@/reauth-middleware'\n\nexport async function middleware(request: NextRequest) {\n  const url = request.nextUrl\n  console.log('Middleware')\n  const response = reauthMiddleware(request)\n  if (response) return response\n\n  return NextResponse.next()\n}\n\nexport const config = {\n  matcher: ['/', '/profile', '/api/:path*'], // или любой другой набор путей\n}\n"
  },
  {
    "path": "apps/nextjs/src/reauth-middleware.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\n\nimport { authApi } from '@/shared/api/auth-api'\nimport { createAccessTokenCookie, createRefreshTokenCookie } from '@/shared/utils/cookieHelpers'\nimport { getJwtExpirationMaxAge, getSecondsToExpiration } from '@/shared/utils/jwt-util'\n\nconst refreshTokens = async (refreshToken: string) => {\n  try {\n    const { accessToken, refreshToken: newRefreshToken } = await authApi.refreshToken({\n      refreshToken,\n    })\n    return { accessToken, refreshToken: newRefreshToken }\n  } catch {\n    return null\n  }\n}\n\nexport async function reauthMiddleware(request: NextRequest) {\n  const accessCookie = request.cookies.get('access-token')\n  const refreshCookie = request.cookies.get('refresh-token')\n  let tokens: { accessToken: string; refreshToken: string } | null = null\n\n  if (accessCookie) {\n    const secondsToExpiration = getSecondsToExpiration(accessCookie.value)\n    if (secondsToExpiration < 20 && refreshCookie) {\n      tokens = await refreshTokens(refreshCookie.value)\n    }\n  } else if (refreshCookie) {\n    tokens = await refreshTokens(refreshCookie.value)\n  }\n\n  const requestHeaders = new Headers(request.headers)\n  if (tokens) {\n    requestHeaders.set('Authorization', 'Bearer ' + tokens.accessToken)\n  }\n\n  const response = NextResponse.next({\n    request: { headers: requestHeaders },\n  })\n\n  if (tokens) {\n    response.cookies.set(createAccessTokenCookie(tokens.accessToken))\n    response.cookies.set(createRefreshTokenCookie(tokens.accessToken))\n  }\n\n  return response\n}\n"
  },
  {
    "path": "apps/nextjs/src/shared/api/auth-api.ts",
    "content": "import { cookies } from 'next/headers'\n\nimport { baseUrl, jsonHeaders } from '@/shared/api/base'\n\nimport type {\n  AuthTokensResponse,\n  MeResponseResponse,\n  OAuthLoginArgs,\n  RefreshTokensArgs,\n} from './authApi.types'\n\n/**\n * Обёртка над fetch, которая проверяет response.ok\n */\nasync function checkResponse<T>(response: Response): Promise<T> {\n  if (!response.ok) {\n    const errorBody = await response.text()\n    throw new Error(`Request failed with status ${response.status}: ${errorBody}`)\n  }\n  return (await response.json()) as T\n}\n\nexport const authApi = {\n  // 1) Login → POST /auth/login\n  async login(payload: OAuthLoginArgs): Promise<AuthTokensResponse> {\n    const response = await fetch(`${baseUrl}/auth/login`, {\n      method: 'POST',\n      headers: jsonHeaders,\n      body: JSON.stringify(payload),\n    })\n    return checkResponse<AuthTokensResponse>(response)\n  },\n\n  // 2) Logout → POST /auth/logout\n  async logout(payload: RefreshTokensArgs): Promise<void> {\n    const response = await fetch(`${baseUrl}/auth/logout`, {\n      method: 'POST',\n      headers: jsonHeaders,\n      body: JSON.stringify(payload),\n    })\n    // не ожидаем JSON в ответе\n    if (!response.ok) {\n      const errorBody = await response.text()\n      throw new Error(`Logout failed with status ${response.status}: ${errorBody}`)\n    }\n  },\n\n  // 3) Получить URL для OAuth-редиректа (без сетевого запроса)\n  oauthUrl(redirectUrl: string): string {\n    // Здесь предполагается, что authEndpoint — это что-то вроде '/api/auth'\n\n    return `${baseUrl}/auth/oauth-redirect?callbackUrl=${encodeURIComponent(redirectUrl)}`\n  },\n\n  // 4) Refresh token → POST /auth/refresh\n  async refreshToken(payload: RefreshTokensArgs): Promise<AuthTokensResponse> {\n    const response = await fetch(`${baseUrl}/auth/refresh`, {\n      method: 'POST',\n      headers: jsonHeaders,\n      body: JSON.stringify(payload),\n    })\n    return checkResponse<AuthTokensResponse>(response)\n  },\n\n  // 5) Get current user → GET /auth/me\n  async getMe(): Promise<MeResponseResponse> {\n    const cookieStore = await cookies()\n    const token = cookieStore.get('access-token')?.value\n\n    const response = await fetch(`${baseUrl}/auth/me`, {\n      method: 'GET',\n      headers: {\n        ...jsonHeaders,\n        Authorization: 'Bearer ' + token,\n      },\n      // Если нужен авторизационный заголовок — добавьте сюда:\n      // headers: {\n      //   Authorization: `Bearer ${localStorage.getItem(\"accessToken\")}`,\n      // },\n    })\n    return checkResponse<MeResponseResponse>(response)\n  },\n}\n"
  },
  {
    "path": "apps/nextjs/src/shared/api/authApi.types.ts",
    "content": "export type MeResponseResponse = {\n  userId: string\n  login: string\n}\n\nexport type AuthTokensResponse = {\n  refreshToken: string\n  accessToken: string\n}\n\nexport type RefreshTokensArgs = {\n  refreshToken: string\n}\n\nexport type OAuthLoginArgs = {\n  code: string\n  redirectUri: string\n  accessTokenTTL: string // e.g. \"3m\"\n  rememberMe: boolean\n}\n\nexport const localStorageKeys = {\n  refreshToken: 'musicfun-refresh-token',\n  accessToken: 'musicfun-access-token',\n}\n"
  },
  {
    "path": "apps/nextjs/src/shared/api/base.ts",
    "content": "export const baseUrl = process.env.BASE_URL!\nexport const apiKey = process.env.API_KEY!\n\nexport const jsonHeaders = {\n  'Content-Type': 'application/json',\n  'api-Key': apiKey,\n  Origin: 'http://localhost:3000',\n}\n\nexport const formHeaders = {\n  'api-Key': apiKey,\n}\n\nexport const redirectAfterOauthUri = 'http://localhost:3000/api/oauth/callback'\n"
  },
  {
    "path": "apps/nextjs/src/shared/api/tracks/tracksApi.ts",
    "content": "import { baseUrl, formHeaders, jsonHeaders } from '@/shared/api/base'\nimport { Nullable } from '@/shared/common.types'\n\nimport type {\n  FetchPlaylistsTracksResponse,\n  FetchTracksArgs,\n  FetchTracksResponse,\n  ReactionResponse,\n  TrackDetailAttributes,\n  TrackDetails,\n  UpdateTrackArgs,\n} from './tracksApi.types.ts'\n\nexport const tracksApi = {\n  async fetchTracks({ pageSize = 3, pageNumber, search = '' }: FetchTracksArgs) {\n    const url = new URL(`${baseUrl}/playlists/tracks`)\n    url.searchParams.set('pageSize', pageSize.toString())\n    url.searchParams.set('pageNumber', pageNumber.toString())\n    url.searchParams.set('search', search)\n\n    const res = await fetch(url.toString(), { headers: jsonHeaders })\n    return res.json() as Promise<FetchTracksResponse>\n  },\n\n  async fetchTracksInPlaylist({ playlistId }: { playlistId: string }) {\n    const url = `${baseUrl}/playlists/${playlistId}/tracks`\n    const res = await fetch(url, { headers: jsonHeaders })\n    return res.json() as Promise<FetchPlaylistsTracksResponse>\n  },\n\n  async fetchTrackById(trackId: string) {\n    const url = `${baseUrl}/playlists/tracks/${trackId}`\n    const res = await fetch(url, { headers: jsonHeaders })\n    return res.json() as Promise<{ data: TrackDetails<TrackDetailAttributes> }>\n  },\n\n  async createTrack({ title, file }: { title: string; file: File }) {\n    const formData = new FormData()\n    formData.append('title', title)\n    formData.append('file', file)\n\n    const url = `${baseUrl}/playlists/tracks/upload`\n    const res = await fetch(url, {\n      method: 'POST',\n      headers: formHeaders,\n      body: formData,\n    })\n    return res.json() as Promise<{ data: TrackDetails<TrackDetailAttributes> }>\n  },\n\n  async removeTrack(trackId: string) {\n    const url = `${baseUrl}/playlists/tracks/${trackId}`\n    await fetch(url, { method: 'DELETE', headers: jsonHeaders })\n  },\n\n  // async uploadTrackCover({ trackId, file }: { trackId: string; file: File }) {\n  //   const formData = new FormData()\n  //   formData.append('cover', file)\n  //\n  //   const url = `${baseUrl}/playlists/tracks/${trackId}/cover`\n  //   const res = await fetch(url, {\n  //     method: 'POST',\n  //     headers: formHeaders,\n  //     body: formData,\n  //   })\n  //   return res.json() as Promise<Cover>\n  // },\n\n  async updateTrack({ trackId, payload }: { trackId: string; payload: UpdateTrackArgs }) {\n    const url = `${baseUrl}/playlists/tracks/${trackId}`\n    const res = await fetch(url, {\n      method: 'PUT',\n      headers: jsonHeaders,\n      body: JSON.stringify(payload),\n    })\n    return res.json() as Promise<{ data: TrackDetails<TrackDetailAttributes> }>\n  },\n\n  async addTrackToPlaylist({ playlistId, trackId }: { playlistId: string; trackId: string }) {\n    const url = `${baseUrl}/playlists/${playlistId}/relationships/tracks`\n    await fetch(url, {\n      method: 'POST',\n      headers: jsonHeaders,\n      body: JSON.stringify({ trackId }),\n    })\n  },\n\n  async removeTrackFromPlaylist({ playlistId, trackId }: { playlistId: string; trackId: string }) {\n    const url = `${baseUrl}/playlists/${playlistId}/relationships/tracks/${trackId}`\n    await fetch(url, { method: 'DELETE', headers: jsonHeaders })\n  },\n\n  async reorderTracks({\n    trackId,\n    playlistId,\n    putAfterItemId,\n  }: {\n    trackId: string\n    playlistId: string\n    putAfterItemId: Nullable<string>\n  }) {\n    const url = `${baseUrl}/playlists/${playlistId}/tracks/${trackId}/reorder`\n    await fetch(url, {\n      method: 'PUT',\n      headers: jsonHeaders,\n      body: JSON.stringify({ putAfterItemId }),\n    })\n  },\n\n  async like(trackId: string) {\n    const url = `${baseUrl}/playlists/tracks/${trackId}/like`\n    const res = await fetch(url, {\n      method: 'POST',\n      headers: jsonHeaders,\n      body: JSON.stringify({}),\n    })\n    return res.json() as Promise<ReactionResponse>\n  },\n\n  async dislike(trackId: string) {\n    const url = `${baseUrl}/playlists/tracks/${trackId}/dislike`\n    const res = await fetch(url, {\n      method: 'POST',\n      headers: jsonHeaders,\n      body: JSON.stringify({}),\n    })\n    return res.json() as Promise<ReactionResponse>\n  },\n}\n"
  },
  {
    "path": "apps/nextjs/src/shared/api/tracks/tracksApi.types.ts",
    "content": "import { Meta, Nullable } from '../../common/types/common.types'\nimport { CurrentUserReaction } from '../../common/types/enums'\nimport { Images, User } from '../../common/types/playlists-tracks.types'\nimport { Artist } from '../artists/artistsApi.types'\nimport { Tag } from '../tags/tagsApi.types'\n\nexport type TrackDetails<T> = {\n  id: string\n  type: 'tracks'\n  attributes: T\n}\n\n// Attributes\nexport type BaseAttributes = {\n  title: string\n  addedAt: string\n  attachments: TrackAttachment[]\n  images: Images\n}\n\nexport type FetchTracksAttributes = BaseAttributes & {\n  user: User\n}\n\nexport type TrackDetailAttributes = BaseAttributes & {\n  lyrics: Nullable<string>\n  releaseDate: Nullable<string>\n  updatedAt: string\n  duration: number\n  processingStatus: TrackProcessingStatus\n  visibility: TrackVisibility\n  tags: Tag[]\n  artists: Artist[]\n  // likes\n  currentUserReaction: CurrentUserReaction\n  dislikesCount: number\n  likesCount: number\n}\n\nexport type PlaylistItemAttributes = BaseAttributes & {\n  updatedAt: string\n  order: number\n}\n\n// Attachment\nexport type TrackAttachment = {\n  id: string\n  addedAt: string\n  updatedAt: string\n  version: number\n  url: string\n  contentType: string\n  originalName: string\n  originalKey: string\n  fileSize: number\n}\n\n// Response\nexport type FetchTracksResponse = {\n  data: TrackDetails<FetchTracksAttributes>[]\n  meta: Meta\n}\n\nexport type FetchPlaylistsTracksResponse = {\n  data: TrackDetails<PlaylistItemAttributes>[]\n  meta: Meta\n}\n\nexport type ReactionResponse = {\n  objectId: string\n  value: number\n  likes: number\n  dislikes: number\n}\n\n// Arguments\nexport type FetchTracksArgs = {\n  pageSize?: number\n  pageNumber: number\n  search?: string\n}\n\nexport type UpdateTrackArgs = {\n  title?: string\n  lyrics?: string\n  visibility?: TrackVisibility\n  releaseDate?: string\n  tagIds?: string[]\n  artistsIds?: string[]\n}\n\n// Literal types\ntype TrackVisibility = 'private' | 'public'\n\ntype TrackProcessingStatus = 'uploaded' | 'converting' | 'ready'\n"
  },
  {
    "path": "apps/nextjs/src/shared/common.types.ts",
    "content": "export type Nullable<T> = T | null\n"
  },
  {
    "path": "apps/nextjs/src/shared/utils/cookieHelpers.ts",
    "content": "// src/shared/utils/cookieHelpers.ts\n\nimport { getJwtExpirationMaxAge } from '@/shared/utils/jwt-util'\n\nexport interface CookieDef {\n  name: string\n  value: string\n  httpOnly: boolean\n  maxAge: number\n  path: string\n}\n\n/**\n * Возвращает определение cookie для access-token\n */\nexport function createAccessTokenCookie(token: string): CookieDef {\n  return {\n    name: 'access-token',\n    value: token,\n    httpOnly: true,\n    maxAge: getJwtExpirationMaxAge(token),\n    path: '/',\n  }\n}\n\n/**\n * Возвращает определение cookie для refresh-token\n */\nexport function createRefreshTokenCookie(token: string): CookieDef {\n  return {\n    name: 'refresh-token',\n    value: token,\n    httpOnly: true,\n    maxAge: getJwtExpirationMaxAge(token),\n    path: '/',\n  }\n}\n"
  },
  {
    "path": "apps/nextjs/src/shared/utils/jwt-util.ts",
    "content": "// src/shared/utils/jwtUtils.ts\n\n/**\n * Распарсить payload JWT и вернуть поле exp\n * @throws Error если формат токена некорректен или нет поля exp\n */\nfunction parseJwtExp(token: string): number {\n  const parts = token.split('.')\n  if (parts.length !== 3) {\n    throw new Error('Invalid JWT format')\n  }\n\n  // Декодируем payload (в middle-segment)\n  const payloadBase64 = parts[1].replace(/-/g, '+').replace(/_/g, '/')\n  const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf-8')\n  const payload = JSON.parse(payloadJson)\n\n  if (typeof payload.exp !== 'number') {\n    throw new Error('JWT payload missing exp claim')\n  }\n\n  return payload.exp\n}\n\n/**\n * Возвращает, сколько секунд осталось до истечения токена\n */\nexport function getSecondsToExpiration(token: string): number {\n  const exp = parseJwtExp(token)\n  const now = Math.floor(Date.now() / 1000)\n  return exp - now\n}\n\n/**\n * Возвращает maxAge для установки в cookie, основанный на exp токена\n * @throws Error если токен уже истёк или exp–now ≤ 0\n */\nexport function getJwtExpirationMaxAge(token: string): number {\n  const seconds = getSecondsToExpiration(token)\n  if (seconds <= 0) {\n    throw new Error('Token already expired')\n  }\n  return seconds\n}\n"
  },
  {
    "path": "apps/nextjs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n.DS_Store\n.env\n/node_modules/\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "apps/react-effector-fsd/README.md",
    "content": "# Welcome to React Router!\n\nA modern, production-ready template for building full-stack React applications using React Router.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)\n\n## Features\n\n- 🚀 Server-side rendering\n- ⚡️ Hot Module Replacement (HMR)\n- 📦 Asset bundling and optimization\n- 🔄 Data loading and mutations\n- 🔒 TypeScript by default\n- 🎉 TailwindCSS for styling\n- 📖 [React Router docs](https://reactrouter.com/)\n\n## Getting Started\n\n### Installation\n\nInstall the dependencies:\n\n```bash\nnpm install\n```\n\n### Development\n\nStart the development server with HMR:\n\n```bash\nnpm run dev\n```\n\nYour application will be available at `http://localhost:5173`.\n\n## Building for Production\n\nCreate a production build:\n\n```bash\nnpm run build\n```\n\n## Deployment\n\n### Docker Deployment\n\nTo build and run using Docker:\n\n```bash\ndocker build -t my-app .\n\n# Run the container\ndocker run -p 3000:3000 my-app\n```\n\nThe containerized application can be deployed to any platform that supports Docker, including:\n\n- AWS ECS\n- Google Cloud Run\n- Azure Container Apps\n- Digital Ocean App Platform\n- Fly.io\n- Railway\n\n### DIY Deployment\n\nIf you're familiar with deploying Node applications, the built-in app server is production-ready.\n\nMake sure to deploy the output of `npm run build`\n\n```\n├── package.json\n├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)\n├── build/\n│   ├── client/    # Static assets\n│   └── server/    # Server-side code\n```\n\n## Styling\n\nThis template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.\n\n---\n\nBuilt with ❤️ using React Router.\n"
  },
  {
    "path": "apps/react-effector-fsd/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport { defineConfig, globalIgnores } from 'eslint/config'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default defineConfig([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs['recommended-latest'],\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "apps/react-effector-fsd/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Musicfun Effector</title>\n    <!-- SPA redirect handler for GitHub Pages -->\n    <script>\n      ;(function () {\n        var redirect = sessionStorage.redirect\n        delete sessionStorage.redirect\n        if (redirect && redirect !== location.href) {\n          history.replaceState(null, null, redirect)\n        }\n\n        // Check for spa_redirect query parameter\n        var searchParams = new URLSearchParams(window.location.search)\n        var spaRedirect = searchParams.get('spa_redirect')\n        if (spaRedirect) {\n          searchParams.delete('spa_redirect')\n          var newSearch = searchParams.toString()\n          var newUrl = decodeURIComponent(spaRedirect)\n          sessionStorage.redirect = newUrl\n          window.location.replace(window.location.pathname + (newSearch ? '?' + newSearch : ''))\n        }\n      })()\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/app/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/react-effector-fsd/package.json",
    "content": "{\n  \"name\": \"react-effector-fsd\",\n  \"private\": true,\n  \"version\": \"0.0.1\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\",\n    \"start\": \"vite ./build/server/index.js\",\n    \"typecheck\": \"tsc --watch\",\n    \"generate:api\": \"pnpm openapi-typescript https://musicfun.it-incubator.app/api-json -o ./src/shared/api/schema.ts --root-types --enum --enum-values --dedupe-enums\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"^7.9.4\",\n    \"@react-router/serve\": \"^7.9.4\",\n    \"clsx\": \"^2.1.1\",\n    \"effector\": \"^23.4.4\",\n    \"effector-react\": \"^23.3.0\",\n    \"isbot\": \"^5.1.31\",\n    \"openapi-fetch\": \"^0.14.1\",\n    \"react\": \"^19.2.0\",\n    \"react-dom\": \"^19.2.0\",\n    \"react-router\": \"^7.9.4\",\n    \"react-toastify\": \"^11.0.5\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"^7.9.4\",\n    \"@storybook/react-vite\": \"^9.1.13\",\n    \"@tailwindcss/vite\": \"^4.1.14\",\n    \"@types/node\": \"^24.7.2\",\n    \"@types/react\": \"^19.2.2\",\n    \"@types/react-dom\": \"^19.2.1\",\n    \"@vitejs/plugin-react\": \"^5.0.4\",\n    \"babel-plugin-react-compiler\": \"^1.0.0\",\n    \"openapi-typescript\": \"^7.9.1\",\n    \"tailwindcss\": \"^4.1.14\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"npm:rolldown-vite@7.1.16\",\n    \"vite-tsconfig-paths\": \"^5.1.4\"\n  },\n  \"overrides\": {\n    \"vite\": \"npm:rolldown-vite@7.1.16\"\n  }\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/App.tsx",
    "content": "import '@/app/styles/fonts.css'\nimport '@/app/styles/variables.css'\nimport '@/app/styles/reset.css'\nimport '@/app/styles/global.css'\n\nimport { useEffect } from 'react'\nimport { ToastContainer } from 'react-toastify'\n\nimport { appStarted } from './model/init.ts'\nimport { Routing } from './routes'\n\nexport default function App() {\n  useEffect(() => {\n    appStarted()\n  }, [])\n\n  return (\n    <>\n      <Routing />\n      <ToastContainer />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/main.tsx",
    "content": "import '@/app/styles/fonts.css'\nimport '@/app/styles/variables.css'\nimport '@/app/styles/reset.css'\nimport '@/app/styles/global.css'\nimport 'react-toastify/dist/ReactToastify.css'\n\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { BrowserRouter } from 'react-router'\n\nimport App from './App.tsx'\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <BrowserRouter basename=\"/effector\">\n      <App />\n    </BrowserRouter>\n  </StrictMode>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/model/init.ts",
    "content": "import { createEvent, sample } from 'effector'\n\nimport { initApiClientFx } from '@/features/auth/model/model.ts'\n\nexport const appStarted = createEvent()\n\nsample({\n  clock: appStarted,\n  target: initApiClientFx,\n})\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/routes/Routing.tsx",
    "content": "import { Route, Routes } from 'react-router'\n\nimport { OAuthCallback } from '@/pages/auth/OAuthRedirect/OAuthCallback.tsx'\nimport { Home } from '@/pages/home'\nimport { UserPage } from '@/pages/user'\nimport { Layout } from '@/widgets/layout'\n\nexport const Routing = () => (\n  <Routes>\n    <Route path=\"/oauth/callback\" element={<OAuthCallback />} />\n    <Route path=\"/\" element={<Layout />}>\n      <Route index element={<Home />} />\n\n      {/*<Route path=\"/tracks\" element={<TracksPage />} />*/}\n      {/*<Route path=\"/tracks/:id\" element={<TrackPage />} />*/}\n\n      {/*<Route path=\"/playlists\" element={<PlaylistsPage />} />*/}\n      {/*<Route path=\"/playlists/:id\" element={<PlaylistPage />} />*/}\n\n      <Route path=\"/user/:id\" element={<UserPage />} />\n    </Route>\n  </Routes>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/routes/index.ts",
    "content": "export { Routing } from './Routing'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/styles/fonts.css",
    "content": "/*\n  source: https://gwfh.mranftl.com/fonts/lato?subsets=latin\n*/\n\n/* lato-regular - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-700 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-900 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 900;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/styles/global.css",
    "content": ":root {\n  font-family: Lato, sans-serif;\n  font-weight: 400;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  line-height: 100%;\n  text-rendering: optimizelegibility;\n\n  font-synthesis: none;\n}\n\n/* Scrollbar styles */\n* {\n  scrollbar-color: var(--color-bg-secondary) var(--color-bg-primary);\n  scrollbar-width: thin;\n}\n\nbody {\n  margin: 0;\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-primary);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/styles/reset.css",
    "content": "/* Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset */\n\n/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Prevent font size inflation */\nhtml {\n  text-size-adjust: none;\n}\n\n/* Remove default margin in favour of better control in authored CSS */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nfigure,\nblockquote,\ndl,\ndd {\n  margin-block-end: 0;\n}\n\nul,\nol {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  line-height: 1.5;\n}\n\n/* Set shorter line heights on headings and interactive elements */\nh1,\nh2,\nh3,\nh4,\nbutton,\ninput,\nlabel {\n  border: none;\n  line-height: 1.1;\n}\n\n/* Balance text wrapping on headings */\nh1,\nh2,\nh3,\nh4 {\n  text-wrap: balance;\n}\n\n/* A elements that don't have a class get default styles */\na {\n  color: currentcolor;\n  text-decoration: none;\n}\n\n/* Make images easier to work with */\nimg,\npicture {\n  display: block;\n  max-width: 100%;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n/* Anything that has been anchored to should have extra scroll margin */\n:target {\n  scroll-margin-block: 5ex;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/app/styles/variables.css",
    "content": ":root {\n  /*\n  * Colors\n  */\n  --color-accent: #ff38b6;\n  --color-disabled: #858585;\n  --color-outline-focus: #1a75f5;\n\n  /* Text */\n  --color-text-primary: #fff;\n  --color-text-primary-reverse: #000;\n  --color-text-secondary: #b3b3b3;\n  --color-text-label: #808080;\n  --color-text-error: #f51a51;\n\n  /* Backgrounds */\n  --color-bg-primary: #000;\n  --color-bg-secondary: #141414;\n  --color-bg-primary-reverse: #fff;\n  --color-bg-input-hover: #262626;\n  --color-bg-card: rgb(7 7 7 / 50%);\n  --color-bg-interactive-secondary: #333;\n\n  /* Borders */\n  --color-border-base: #7f7f7f;\n  --color-border-input-primary: #4d4d4d;\n  --color-border-input-active: #fffefe;\n\n  /*\n  * Typography\n  */\n\n  /* font-sizes */\n  --font-size-xxxs: 12px;\n  --font-size-xxs: 13px;\n  --font-size-xs: 14px;\n  --font-size-s: 16px;\n  --font-size-m: 18px;\n  --font-size-l: 20px;\n  --font-size-xl: 24px;\n  --font-size-xxl: 30px;\n  --font-size-xxxl: 60px;\n\n  /*\n  * Layout\n  */\n  --header-height: 80px;\n  --player-height: 112px;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/api/login.ts",
    "content": "import { api } from '@/shared/api/client'\n\nimport type { LoginRequestPayload, RefreshOutput } from '../model/auth-api.types'\n\nexport const loginApi = async (payload: LoginRequestPayload): Promise<RefreshOutput> => {\n  const res = await api().POST('/auth/login', {\n    body: payload,\n  })\n\n  if (res.error) {\n    throw new Error(res.error?.message)\n  }\n\n  return res.data\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/api/logout.ts",
    "content": "import { api } from '@/shared/api/client'\n\nexport const logoutApi = async (refreshToken: string) => {\n  await api().POST('/auth/logout', {\n    body: { refreshToken },\n  })\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/api/me.ts",
    "content": "import { api } from '@/shared/api/client'\n\nimport type { User } from '../model/user.types'\n\nexport const meApi = async (): Promise<User> => {\n  const res = await api().GET('/auth/me')\n  return res.data ?? null\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/index.ts",
    "content": "export { getOauthRedirectUrl } from './model/auth-api.types'\nexport { $me, loginFx, logoutFx } from './model/model.ts'\nexport * from './ui'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/model/auth-api.types.ts",
    "content": "import { getClientConfig } from '@/shared/api/client.ts'\nimport type { components } from '@/shared/api/schema.ts'\n\nexport type RefreshOutput = components['schemas']['RefreshOutput']\nexport type RefreshRequestPayload = components['schemas']['RefreshRequestPayload']\nexport type LoginRequestPayload = components['schemas']['LoginRequestPayload']\n\nexport const localStorageKeys = {\n  refreshToken: 'spotifun-refresh-token',\n  accessToken: 'spotifun-access-token',\n}\n\nexport const getOauthRedirectUrl = (redirectUrl: string) =>\n  getClientConfig().baseURL + `/auth/oauth-redirect?callbackUrl=${redirectUrl}`\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/model/model.ts",
    "content": "import { createEffect, createStore, sample } from 'effector'\nimport { toast } from 'react-toastify'\n\nimport { setClientConfig } from '@/shared/api/client.ts'\nimport { API_BASE_URL, API_KEY } from '@/shared/config/config.ts'\n\nimport { loginApi } from '../api/login'\nimport { logoutApi } from '../api/logout'\nimport { meApi } from '../api/me'\nimport { localStorageKeys, type LoginRequestPayload, type RefreshOutput } from './auth-api.types'\nimport type { User } from './user.types'\n\nexport const initApiClientFx = createEffect(() => {\n  setClientConfig({\n    baseURL: API_BASE_URL,\n    apiKey: API_KEY,\n    getAccessToken: async () => localStorage.getItem(localStorageKeys.accessToken),\n    getRefreshToken: async () => localStorage.getItem(localStorageKeys.refreshToken),\n    saveAccessToken: async (token) =>\n      token\n        ? localStorage.setItem(localStorageKeys.accessToken, token)\n        : localStorage.removeItem(localStorageKeys.accessToken),\n    saveRefreshToken: async (token) =>\n      token\n        ? localStorage.setItem(localStorageKeys.refreshToken, token)\n        : localStorage.removeItem(localStorageKeys.refreshToken),\n\n    toManyRequestsErrorHandler: (message: string | null) => {\n      toast(message)\n    },\n    logoutHandler: () => {},\n  })\n})\n\nexport const fetchMeFx = createEffect<void, User>(meApi)\nexport const loginFx = createEffect<LoginRequestPayload, RefreshOutput>(loginApi)\nexport const logoutFx = createEffect(async () => {\n  const refreshToken = localStorage.getItem(localStorageKeys.refreshToken)!\n  await logoutApi(refreshToken)\n})\n\nconst saveTokensFx = createEffect((data: RefreshOutput) => {\n  localStorage.setItem(localStorageKeys.refreshToken, data.refreshToken)\n  localStorage.setItem(localStorageKeys.accessToken, data.accessToken)\n})\nconst clearTokensFx = createEffect(() => {\n  localStorage.removeItem(localStorageKeys.accessToken)\n  localStorage.removeItem(localStorageKeys.refreshToken)\n})\n\nexport const $me = createStore<User>(null)\n  .on(fetchMeFx.doneData, (_, me) => me)\n  .reset(logoutFx.done)\n\nexport const $isAuthorized = createStore<boolean>(false)\n  .on(fetchMeFx.doneData, (_, me) => Boolean(me))\n  .reset(logoutFx.done)\n\nsample({\n  clock: initApiClientFx.done,\n  filter: () => Boolean(localStorage.getItem(localStorageKeys.accessToken)),\n  target: fetchMeFx,\n})\n\nsample({\n  clock: loginFx.doneData,\n  target: saveTokensFx,\n})\nsample({\n  clock: saveTokensFx.done,\n  target: fetchMeFx,\n})\n\nsample({\n  clock: logoutFx.done,\n  target: clearTokensFx,\n})\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/model/user.types.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\nexport type User = components['schemas']['GetMeOutput'] | null\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/ui/LoginButtonAndModal/LoginButtonAndModal.module.css",
    "content": ".dialog {\n  width: 376px;\n  padding-bottom: 22px;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n  align-items: center;\n\n  text-align: center;\n}\n\n.icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 60px;\n  height: 60px;\n  border-radius: 50%;\n\n  font-size: 24px;\n\n  background-color: var(--color-accent);\n}\n\n.button {\n  height: 55px;\n}\n\n.secondary {\n  background-color: #555;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/ui/LoginButtonAndModal/LoginButtonAndModal.tsx",
    "content": "import clsx from 'clsx'\nimport { useUnit } from 'effector-react'\nimport { useState } from 'react'\n\nimport { getOauthRedirectUrl, loginFx } from '@/features/auth'\nimport { Button } from '@/shared/components/Button'\nimport { Dialog, DialogContent, DialogHeader } from '@/shared/components/Dialog'\nimport { Typography } from '@/shared/components/Typography'\nimport { CURRENT_APP_DOMAIN } from '@/shared/config/config'\n\nimport s from './LoginButtonAndModal.module.css'\n\nexport const LoginButtonAndModal = () => {\n  const [isOpen, setIsOpen] = useState(false)\n  const [login, loginPending] = useUnit([loginFx, loginFx.pending])\n\n  const handleOpenModal = () => setIsOpen(true)\n  const handleCloseModal = () => setIsOpen(false)\n\n  const loginHandler = () => {\n    const redirectUri = window.location.origin + CURRENT_APP_DOMAIN + 'oauth/callback' // todo: to config\n    const url = getOauthRedirectUrl(redirectUri)\n    window.open(url, 'oauthPopup', 'width=500,height=600')\n\n    const receiveMessage = async (event: MessageEvent) => {\n      if (event.origin !== window.location.origin) {\n        // todo: to config\n        return\n      }\n\n      const { code } = event.data\n      if (code) {\n        console.log('✅ code received:', code)\n        window.removeEventListener('message', receiveMessage)\n\n        // effector login\n        login({ code, accessTokenTTL: '10m', redirectUri, rememberMe: true })\n        handleCloseModal()\n      }\n    }\n\n    window.addEventListener('message', receiveMessage)\n  }\n\n  return (\n    <>\n      <Button variant=\"primary\" onClick={handleOpenModal}>\n        Sign in\n      </Button>\n\n      <Dialog open={isOpen} onClose={handleCloseModal} className={s.dialog}>\n        <DialogHeader />\n\n        <DialogContent className={s.content}>\n          <Typography variant=\"h2\">\n            Millions of Songs. <br /> Free on Musicfun.\n          </Typography>\n\n          <div className={s.icon}>😊</div>\n\n          <Button className={clsx(s.button, s.secondary)} fullWidth onClick={handleCloseModal}>\n            Continue without Sign in\n          </Button>\n          <Button\n            as=\"button\"\n            target=\"_blank\"\n            className={s.button}\n            variant=\"primary\"\n            fullWidth\n            onClick={loginHandler}\n            disabled={loginPending}>\n            {loginPending ? 'Signing in...' : 'Sign in with APIHub'}\n          </Button>\n        </DialogContent>\n      </Dialog>\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/ui/LoginButtonAndModal/index.ts",
    "content": "export { LoginButtonAndModal } from './LoginButtonAndModal'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.module.css",
    "content": ".trigger {\n  cursor: pointer;\n  display: flex;\n  gap: 8px;\n  align-items: center;\n}\n\n.avatar {\n  overflow: hidden;\n  width: 34px;\n  height: 34px;\n  border-radius: 50%;\n}\n\n.name {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ProfileDropdownMenu } from './ProfileDropdownMenu'\n\nconst meta: Meta<typeof ProfileDropdownMenu> = {\n  title: 'entities/ProfileDropdownMenu',\n  component: ProfileDropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof ProfileDropdownMenu>\n\nexport const Default: Story = {\n  args: {\n    avatar: 'https://unsplash.it/182/182',\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.tsx",
    "content": "import { useUnit } from 'effector-react'\nimport { Link } from 'react-router'\n\nimport { $me, logoutFx } from '@/features/auth'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Typography,\n} from '@/shared/components'\nimport { LogoutIcon, ProfileIcon } from '@/shared/icons'\n\nimport s from './ProfileDropdownMenu.module.css'\n\nexport const ProfileDropdownMenu = ({ avatar }: { avatar: string }) => {\n  const [me, logout, logoutPending] = useUnit([$me, logoutFx, logoutFx.pending])\n\n  const handleLogout = () => {\n    logout()\n  }\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild className={s.trigger}>\n        <div className={s.avatar}>\n          <img src={avatar} alt={''} />\n        </div>\n        <Typography className={s.name} variant=\"body2\">\n          {me?.login ?? 'anonymous'}\n        </Typography>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem as={Link} to={`/user/${me!.userId}`}>\n          <ProfileIcon />\n          <span>My Profile</span>\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={handleLogout} disabled={logoutPending}>\n          <LogoutIcon />\n          <span>{logoutPending ? 'Logging out...' : 'Logout'}</span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/ui/ProfileDropdownMenu/index.ts",
    "content": "export * from './ProfileDropdownMenu'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/features/auth/ui/index.ts",
    "content": "export * from './LoginButtonAndModal'\nexport * from './ProfileDropdownMenu'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/pages/auth/OAuthRedirect/OAuthCallback.module.css",
    "content": ".title {\n  text-align: center;\n  font-size: 250px;\n  margin: 0;\n}\n\n.subtitle {\n  text-align: center;\n  font-size: 50px;\n  margin: 0;\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/pages/auth/OAuthRedirect/OAuthCallback.tsx",
    "content": "import { useEffect } from 'react'\n\nexport const OAuthCallback = () => {\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const code = url.searchParams.get('code') // или code/state, если flow другой\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, '*') // Лучше заменить \"*\" на точный origin\n    }\n\n    window.close()\n  }, [])\n\n  return <p>Welcome...</p>\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/pages/home/Home.tsx",
    "content": "export function Home() {\n  return <div>Musicfun home page</div>\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/pages/home/index.ts",
    "content": "export { Home } from './Home'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/pages/user/UserPage.tsx",
    "content": "export const UserPage = () => {\n  return (\n    <main>\n      <div>UserPage</div>\n    </main>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/pages/user/index.ts",
    "content": "export { UserPage } from './UserPage'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/api/client.ts",
    "content": "import createClient, { type Middleware } from 'openapi-fetch'\n\nimport type { paths } from './schema.ts'\n\nconst config = {\n  baseURL: null as string | null,\n  apiKey: null as string | null,\n  getAccessToken: null as (() => Promise<string | null>) | null,\n  saveAccessToken: null as ((accessToken: string | null) => Promise<void>) | null,\n  getRefreshToken: null as (() => Promise<string | null>) | null,\n  saveRefreshToken: null as ((refreshToken: string | null) => Promise<void>) | null,\n  toManyRequestsErrorHandler: null as ((message: string | null) => void) | null,\n  logoutHandler: null as (() => void) | null,\n}\n\nexport const setClientConfig = (newConfig: Partial<typeof config>) => {\n  Object.assign(config, newConfig)\n  _client = undefined // пере-инициализируем\n}\n\nexport const getClientConfig = () => ({ ...config })\n\n/* ------------------------------------------------------------------ */\n/* 2.  Mutex для refresh-а                                             */\n/* ------------------------------------------------------------------ */\nlet refreshPromise: Promise<string> | null = null\n\nexport function makeRefreshToken(): Promise<string> {\n  if (!refreshPromise) {\n    // 1) создаём «замок» сразу\n    refreshPromise = (async (): Promise<string> => {\n      const refreshToken = await config.getRefreshToken!()\n      if (!refreshToken) throw new Error('No refresh token')\n\n      const res = await fetch(`${config.baseURL}/auth/refresh`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'API-KEY': config.apiKey!,\n        },\n        body: JSON.stringify({ refreshToken }),\n      })\n      if (res.status !== 201) throw new Error('Refresh failed')\n\n      const { accessToken, refreshToken: newRT } = await res.json()\n      await config.saveAccessToken!(accessToken)\n      await config.saveRefreshToken!(newRT)\n\n      return accessToken\n    })().finally(() => {\n      refreshPromise = null // 2) снимаем «замок»\n    })\n  }\n\n  return refreshPromise\n}\n\nconst authMiddleware: Middleware = {\n  /* ---------- REQUEST -------------------------------------------------- */\n  async onRequest({ request }) {\n    request.headers.set('API-KEY', config.apiKey!)\n\n    const token = await config.getAccessToken?.()\n    if (token) request.headers.set('Authorization', `Bearer ${token}`)\n    ;(request as any)._retryClone = request.clone()\n\n    return request\n  },\n  async onResponse({ request, response }) {\n    const req = request as Request & { _retry: boolean }\n\n    if (response.status === 429) {\n      const { message } = await response.clone().json()\n      config.toManyRequestsErrorHandler?.(message)\n    }\n\n    if (response.status !== 401 || request.url.includes('/auth/refresh')) {\n      return response // всё ок\n    }\n\n    // уже пытались? -> отдаём 401 наружу, чтобы не зациклиться\n    if (req._retry) return response\n    req._retry = true\n\n    try {\n      const newToken = await makeRefreshToken()\n\n      // повторяем исходный запрос с новым токеном\n      const orig = (req as any)._retryClone as Request // клон с целым body\n      const retry = new Request(orig, { headers: new Headers(orig.headers) })\n      retry.headers.set('Authorization', `Bearer ${newToken}`)\n      return await fetch(retry)\n    } catch (error) {\n      console.log(error)\n      // refresh не удался → чистим хранилище, отдаём 401\n      await config.saveAccessToken!(null)\n      await config.saveRefreshToken!(null)\n      await config.logoutHandler?.()\n      return response\n    }\n  },\n}\n\nlet _client: ReturnType<typeof createClient<paths>> | undefined\n\nconst LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0'])\n\nfunction isLocalClient(): boolean {\n  if (typeof window === 'undefined') return false // не клиент\n  const h = window.location.hostname\n  return LOCAL_HOSTNAMES.has(h) || h.endsWith('.localhost')\n}\n\nexport function assertApiConfig() {\n  if (!config.baseURL) {\n    const msg = 'baseURL is required. Call setClientConfig({ baseURL })'\n    console.error(msg)\n    throw new Error(msg)\n  }\n  if (isLocalClient() && !config.apiKey) {\n    const msg =\n      'apiKey is required when running client on localhost. Call setClientConfig({ apiKey })'\n    console.error(msg)\n    throw new Error(msg)\n  }\n}\n\nexport const api = () => {\n  if (_client) return _client\n\n  assertApiConfig()\n\n  const client = createClient<paths>({ baseUrl: config.baseURL! })\n  client.use(authMiddleware)\n  _client = client\n  return _client\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/api/schema.ts",
    "content": "/**\n * This file was auto-generated by openapi-typescript.\n * Do not make direct changes to the file.\n */\n\nexport interface paths {\n  '/playlists/my': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Get my playlists\n     * @deprecated\n     */\n    get: operations['PlaylistsController_getMyPlaylists']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Retrieve all playlists\n     * @description Query parameters must conform to the **GetPlaylistsRequestPayload** schema.\n     */\n    get: operations['PlaylistsPublicController_getPlaylists']\n    put?: never\n    /** Create a new playlist */\n    post: operations['PlaylistsController_createPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get a single playlist by ID */\n    get: operations['PlaylistsPublicController_getPlaylistById']\n    /** Update a playlist */\n    put: operations['PlaylistsController_updatePlaylist']\n    post?: never\n    /** Delete a playlist */\n    delete: operations['PlaylistsController_deletePlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Reorder playlists */\n    put: operations['PlaylistsController_reorderPlaylist']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/images/main': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /**\n     * Upload playlist cover\n     * @description Minimum height — 500px; image must be square\n     */\n    post: operations['PlaylistsController_uploadMainImage']\n    /** Delete playlist cover */\n    delete: operations['PlaylistsController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Get list of all tracks in all playlists.\n     * @description Query-parameters schema → [`GetTracksRequestPayload`](#model-GetTracksRequestPayload)\n     */\n    get: operations['TracksPublicController_getAllTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get list of tracks in a playlist */\n    get: operations['TracksPublicController_getPlaylistTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get track details by ID */\n    get: operations['TracksPublicController_getTrackDetails']\n    /** Update track information */\n    put: operations['TracksController_updateTrack']\n    post?: never\n    /** Permanently delete a track */\n    delete: operations['TracksController_deleteTrackCompletely']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Like or toggle like on a track */\n    post: operations['TracksPublicController_likeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Dislike or toggle dislike on a track */\n    post: operations['TracksPublicController_dislikeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove user reaction from a track */\n    delete: operations['TracksPublicController_removeTrackReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Like a playlist */\n    post: operations['PlaylistsPublicController_likePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Dislike a playlist */\n    post: operations['PlaylistsPublicController_dislikePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove user reaction from a playlist */\n    delete: operations['PlaylistsPublicController_removePlaylistReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks/{trackId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Reorder tracks in a playlist */\n    put: operations['TracksController_reorderTrack']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Add a track to your playlist */\n    post: operations['TracksController_addTrackToPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove a track from your playlist */\n    delete: operations['TracksController_unbindTrackFromPlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/actions/publish': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Publish a track (make it publicly available) */\n    post: operations['TracksController_publishTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/cover': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Upload track cover */\n    post: operations['TracksController_uploadTrackCover']\n    /** Delete track cover */\n    delete: operations['TracksController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/upload': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a track with MP3 file upload */\n    post: operations['TracksController_uploadTrackMp3']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a new artist */\n    post: operations['ArtistsController_createArtist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Search artists by substring */\n    get: operations['ArtistsController_searchArtist']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Delete an artist by ID */\n    delete: operations['ArtistsController_deleteArtist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/oauth-redirect': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * OAuth redirect\n     * @description The callback URL to redirect after granting access, <a target=\"_blank\" href=\"https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\">https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid</a>\n     */\n    get: operations['AuthController_OauthRedirect']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/login': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Log in using the code received after OAuth authorization redirect */\n    post: operations['AuthController_login']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/refresh': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Refresh refresh/access token pair */\n    post: operations['AuthController_refresh']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/logout': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Deactivate refresh token */\n    post: operations['AuthController_logout']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/me': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get current user by access token */\n    get: operations['AuthController_getMe']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a new tag */\n    post: operations['TagsController_createTag']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Search tags by substring */\n    get: operations['TagsController_searchTags']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Delete a tag by ID */\n    delete: operations['TagsController_deleteTag']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n}\nexport type webhooks = Record<string, never>\nexport interface components {\n  schemas: {\n    UserRef: {\n      /** @description Unique identifier of the user */\n      id: string\n      /** @description Name of the user */\n      name: string\n    }\n    /**\n     * @description Type of the image size (e.g., original, thumbnail variants)\n     * @enum {string}\n     */\n    ImageSizeType: ImageSizeType\n    ImageVariant: {\n      /** @description Type of the image size (e.g., original, thumbnail variants) */\n      type: components['schemas']['ImageSizeType']\n      /** @description Image width in pixels */\n      width: number\n      /** @description Image height in pixels */\n      height: number\n      /** @description Image file size in bytes */\n      fileSize: number\n      /** @description Full public URL of the image */\n      url: string\n    }\n    PlaylistImagesOutputDTO: {\n      /** @description Original images and thumbnail previews */\n      main?: components['schemas']['ImageVariant'][]\n    }\n    TagRef: {\n      /** @description Unique identifier of the tag */\n      id: string\n      /** @description Original name of the tag */\n      name: string\n    }\n    /**\n     * @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike\n     * @enum {number}\n     */\n    ReactionValue: ReactionValue\n    PlaylistListItemAttributes: {\n      /** @description Title of the playlist */\n      title: string\n      /** @description Description of the playlist */\n      description: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Order index of the playlist */\n      order: number\n      /** @description User who created the playlist */\n      user: components['schemas']['UserRef']\n      /** @description Images associated with the playlist */\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      /** @description Tags linked to the playlist */\n      tags: components['schemas']['TagRef'][]\n      /** @description Total number of likes for this playlist */\n      likesCount: number\n      /** @description Total number of dislikes for this playlist */\n      dislikesCount: number\n      /** @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistListItemResource: {\n      /** @description Unique identifier of the playlist */\n      id: string\n      /**\n       * @description Resource type (should be \"playlists\")\n       * @example playlists\n       */\n      type: string\n      attributes: components['schemas']['PlaylistListItemAttributes']\n    }\n    GetMyPlaylistsOutput: {\n      /** @description Array of playlist resource objects owned by the current user */\n      data: components['schemas']['PlaylistListItemResource'][]\n    }\n    CreatePlaylistRequestPayload: {\n      /** @description Playlist title (1 to 100 characters) */\n      title: string\n      /** @description Playlist description (up to 1000 characters) */\n      description: string | null\n    }\n    PlaylistAttributes: {\n      /** @description Title of the playlist */\n      title: string\n      /** @description Description of the playlist */\n      description: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Order index of the playlist */\n      order: number\n      /** @description User who created the playlist */\n      user: components['schemas']['UserRef']\n      /** @description Images associated with the playlist */\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      /** @description Tags linked to the playlist */\n      tags: components['schemas']['TagRef'][]\n      /** @description Total number of likes for this playlist */\n      likesCount: number\n      /** @description Total number of dislikes for this playlist */\n      dislikesCount: number\n      /** @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistResource: {\n      /** @description Unique identifier of the playlist */\n      id: string\n      /**\n       * @description Resource type (should be \"playlists\")\n       * @example playlists\n       */\n      type: string\n      attributes: components['schemas']['PlaylistAttributes']\n    }\n    GetPlaylistOutput: {\n      data: components['schemas']['PlaylistResource']\n    }\n    UpdatePlaylistRequestPayload: {\n      /** @description Playlist title (1 – 100 characters) */\n      title: string\n      /**\n       * @description Playlist description (up to 1000 characters)\n       * @example Cool playlist\n       */\n      description: string | null\n      /** @description Tag IDs to associate with the playlist (0 – 5 items; [] = clear tags) */\n      tagIds: string[]\n    }\n    ReorderPlaylistsRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID of the playlist after which the current playlist should be inserted. Send null to place the playlist at the beginning of the list.\n       */\n      putAfterItemId: string | null\n    }\n    TrackImages: {\n      /** @description List of original images and thumbnail versions (e.g., original, 320x180, etc.) */\n      main?: components['schemas']['ImageVariant'][]\n    }\n    GetTracksRequestPayload: {\n      /**\n       * @description Page number for pagination (starting from 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Page size for pagination (between 1 and 20)\n       * @default 20\n       */\n      pageSize: number\n      /** @description Search term for filtering playlists by name */\n      search?: string\n      /**\n       * @description Field by which to sort tracks\n       * @default publishedAt\n       * @enum {string}\n       */\n      sortBy: PathsPlaylistsTracksGetParametersQuerySortBy\n      /**\n       * @description Sort direction (ascending or descending)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection\n      /** @description Filter by tag IDs (multiple values allowed) */\n      tagsIds?: string[]\n      /** @description Filter by artist IDs (multiple values allowed) */\n      artistsIds?: string[]\n      /** @description Filter by user ID (track creator's ID) */\n      userId?: string\n      /** @description If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n      includeDrafts?: boolean\n      /**\n       * @description Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination.\n       * @default offset\n       * @enum {string}\n       */\n      paginationType: PathsPlaylistsTracksGetParametersQueryPaginationType\n      /** @description Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\". */\n      cursor?: string | null\n    }\n    JsonApiErrorSource: {\n      /**\n       * @description e.g. \"/data/attributes/field\"\n       * @example /data/attributes/field\n       */\n      pointer?: string\n      /**\n       * @description e.g. \"?queryParam\"\n       * @example ?queryParam\n       */\n      parameter?: string\n    }\n    JsonApiError: {\n      /**\n       * @description HTTP status code as a string\n       * @example 404\n       */\n      status: string\n      /**\n       * @description Application-specific error code\n       * @example E123\n       */\n      code?: Record<string, never>\n      /**\n       * @description Short, human-readable summary\n       * @example Not Found\n       */\n      title?: string\n      /**\n       * @description Detailed explanation\n       * @example User with ID 123 not found\n       */\n      detail?: string\n      /** @description Pointer to the associated entity in the request */\n      source?: components['schemas']['JsonApiErrorSource']\n      /** @description Any extra data */\n      meta?: Record<string, never>\n    }\n    JsonApiErrorDocument: {\n      /** @description Array of one or more errors */\n      errors: components['schemas']['JsonApiError'][]\n      /** @description e.g. timestamp, path, traceId, etc. */\n      meta?: Record<string, never>\n    }\n    TrackAttachment: {\n      /** @description Unique identifier of the entity */\n      id: string\n      /**\n       * Format: date-time\n       * @description Date and time when the entity was added\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the entity was last updated\n       */\n      updatedAt: string\n      /** @description Version number of the entity (for concurrency control) */\n      version: number\n      /**\n       * @description Public URL to access the uploaded file\n       * @example https://cdn.example.com/uploads/track123/cover.jpg\n       */\n      url: string\n      /**\n       * @description MIME type of the file\n       * @example image/jpeg\n       */\n      contentType: string\n      /**\n       * @description Original filename uploaded by the user\n       * @example cover.jpg\n       */\n      originalName: string\n      /**\n       * @description Size of the file in bytes\n       * @example 34872\n       */\n      fileSize: number\n    }\n    TrackListItemAttributes: {\n      title: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      likesCount: number\n      attachments: components['schemas']['TrackAttachment'][]\n      images: components['schemas']['TrackImages']\n      user: components['schemas']['UserRef']\n      /**\n       * @description 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк\n       * @enum {number}\n       */\n      currentUserReaction: ReactionValue\n      isPublished: boolean\n      /**\n       * Format: date-time\n       * @description Date and time when the track was published (ISO 8601)\n       */\n      publishedAt?: string | null\n    }\n    ArtistRelationship: {\n      id: string\n      type: string\n    }\n    ArtistsRelationship: {\n      data: components['schemas']['ArtistRelationship'][]\n    }\n    TrackRelationships: {\n      artists: components['schemas']['ArtistsRelationship']\n    }\n    TrackListItemResource: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackListItemAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMetaWithPagingAndCursor: {\n      page: number\n      pageSize: number\n      /** @description Total count may be absent when using keyset pagination */\n      totalCount: number | null\n      /** @description Total number of pages */\n      pagesCount: number | null\n      /** @description Cursor for the next page */\n      nextCursor: string | null\n    }\n    OmitTypeClass: {\n      /** @description Name of the artist */\n      name: string\n    }\n    IncludedArtistOutput: {\n      id: string\n      type: string\n      attributes: components['schemas']['OmitTypeClass']\n    }\n    GetTrackListOutput: {\n      data: components['schemas']['TrackListItemResource'][]\n      meta: components['schemas']['JsonApiMetaWithPagingAndCursor']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    TrackListItemAttributesForPlaylist: {\n      /** @description Title of the track */\n      title: string\n      /** @description Order index of the track in the playlist */\n      order: number\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Attachments related to the track */\n      attachments: components['schemas']['TrackAttachment'][]\n      /** @description Images associated with the track */\n      images: components['schemas']['TrackImages']\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – liked; -1 – disliked\n       * @enum {number|null}\n       */\n      currentUserReaction: ReactionValue\n      /**\n       * Format: date-time\n       * @description Date and time when the track was published (ISO 8601)\n       */\n      publishedAt?: string | null\n    }\n    TrackListItemResourceForPlaylist: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackListItemAttributesForPlaylist']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMeta: {\n      totalCount: number\n    }\n    GetTracksForPlaylistOutput: {\n      data: components['schemas']['TrackListItemResourceForPlaylist'][]\n      meta: components['schemas']['JsonApiMeta']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    ArtistRef: {\n      /** @description Unique identifier of the artist */\n      id: string\n      /** @description Name of the artist */\n      name: string\n    }\n    TrackDetailsAttributes: {\n      /** @description Track title */\n      title: string\n      /** @description Track lyrics text */\n      lyrics?: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate?: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Duration of the track in seconds */\n      duration: number\n      /** @description Total number of likes for this track */\n      likesCount: number\n      /**\n       * @deprecated\n       * @description Total number of dislikes for this track\n       */\n      dislikesCount: number\n      /** @description List of attachments related to the track */\n      attachments: components['schemas']['TrackAttachment'][]\n      images: components['schemas']['TrackImages']\n      /** @description Tags associated with the track */\n      tags: components['schemas']['TagRef'][]\n      /** @description Artists associated with the track */\n      artists: components['schemas']['ArtistRef'][]\n      user: components['schemas']['UserRef']\n      /** @description Publication status of the track */\n      isPublished: boolean\n      /**\n       * Format: date-time\n       * @description Publication date in ISO 8601 format\n       */\n      publishedAt?: string | null\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n       * @enum {number}\n       */\n      currentUserReaction: ReactionValue\n    }\n    TrackDetailsResource: {\n      /** @description Unique identifier of the track */\n      id: string\n      /**\n       * @description Resource type (should be \"tracks\")\n       * @example tracks\n       */\n      type: string\n      attributes: components['schemas']['TrackDetailsAttributes']\n    }\n    GetTrackDetailsOutput: {\n      data: components['schemas']['TrackDetailsResource']\n    }\n    ReactionOutput: {\n      objectId: string\n      /** @enum {number} */\n      value: ReactionValue\n      likes: number\n      dislikes: number\n    }\n    GetPlaylistsRequestPayload: {\n      /**\n       * @description Page number for pagination (starting from 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Page size for pagination (between 1 and 20)\n       * @default 20\n       */\n      pageSize: number\n      /** @description Search term for filtering playlists by name */\n      search?: string\n      /**\n       * @description Field by which to sort playlists\n       * @default addedAt\n       * @enum {string}\n       */\n      sortBy: PathsPlaylistsGetParametersQuerySortBy\n      /**\n       * @description Sort direction (ascending or descending)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection\n      /** @description Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n      tagsIds?: string[]\n      /** @description Filter by user ID (playlist creator’s ID) */\n      userId?: string\n      /** @description Filter by track ID – only playlists containing this track will be returned */\n      trackId?: string\n    }\n    JsonApiMetaWithPaging: {\n      totalCount: number\n      page: number\n      pageSize: number\n      pagesCount: number\n    }\n    GetPlaylistsOutput: {\n      /** @description Array of playlist resource objects */\n      data: components['schemas']['PlaylistListItemResource'][]\n      meta: components['schemas']['JsonApiMetaWithPaging']\n    }\n    ReorderTracksRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID of the track after which the current track should be inserted. Send null to place the track at the beginning of the list.\n       * @example a1b2c3d4-e5f6-7890-abcd-1234567890ef\n       */\n      putAfterItemId: string | null\n    }\n    UpdateTrackRequestPayload: {\n      /** @description Track title (1 to 100 characters) */\n      title: string\n      /** @description Track lyrics (up to 5000 characters) */\n      lyrics: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate: string | null\n      /** @description Array of tag IDs to associate with the track (up to 5) */\n      tagIds: string[]\n      /** @description Array of artist IDs to associate with the track (up to 5) */\n      artistsIds: string[]\n    }\n    AddTrackToPlaylistRequestPayload: {\n      /** @description ID of the track to add to the playlist */\n      trackId: string\n    }\n    CreateArtistRequestPayload: {\n      /** @description Artist name (must be between 2 and 30 characters) */\n      name: string\n    }\n    LoginRequestPayload: {\n      /** @description Authorization code received from OAuth server after redirect */\n      code: string\n      /**\n       * @description Specify the same redirect URI used in the initial OAuth server request\n       * @example http://localhost:3000/oauth2/callback\n       */\n      redirectUri: string\n      /**\n       * @description Access token lifetime (default \"3m\"); must be a string like \"60s\", \"3m\", \"2h\", or \"1d\"\n       * @example 3m\n       */\n      accessTokenTTL?: string\n      /** @description Refresh token lifetime: if true, 30 days; if false, 30 minutes. accessTokenTTL must not exceed the refresh token lifetime */\n      rememberMe: boolean\n    }\n    RefreshOutput: {\n      refreshToken: string\n      accessToken: string\n    }\n    BadRequestException: Record<string, never>\n    UnauthorizedException: Record<string, never>\n    RefreshRequestPayload: {\n      refreshToken: string\n    }\n    LogoutRequestPayload: {\n      refreshToken: string\n    }\n    GetMeOutput: {\n      userId: string\n      login: string\n    }\n    CreateTagRequestPayload: {\n      /** @description Tag name (2 to 30 characters) */\n      name: string\n    }\n    /**\n     * Format: binary\n     * @description Файл в multipart/form-data\n     */\n    BinaryFile: string\n  }\n  responses: never\n  parameters: never\n  requestBodies: never\n  headers: never\n  pathItems: never\n}\nexport type SchemaUserRef = components['schemas']['UserRef']\nexport type SchemaImageVariant = components['schemas']['ImageVariant']\nexport type SchemaPlaylistImagesOutputDto = components['schemas']['PlaylistImagesOutputDTO']\nexport type SchemaTagRef = components['schemas']['TagRef']\nexport type SchemaPlaylistListItemAttributes = components['schemas']['PlaylistListItemAttributes']\nexport type SchemaPlaylistListItemResource = components['schemas']['PlaylistListItemResource']\nexport type SchemaGetMyPlaylistsOutput = components['schemas']['GetMyPlaylistsOutput']\nexport type SchemaCreatePlaylistRequestPayload =\n  components['schemas']['CreatePlaylistRequestPayload']\nexport type SchemaPlaylistAttributes = components['schemas']['PlaylistAttributes']\nexport type SchemaPlaylistResource = components['schemas']['PlaylistResource']\nexport type SchemaGetPlaylistOutput = components['schemas']['GetPlaylistOutput']\nexport type SchemaUpdatePlaylistRequestPayload =\n  components['schemas']['UpdatePlaylistRequestPayload']\nexport type SchemaReorderPlaylistsRequestPayload =\n  components['schemas']['ReorderPlaylistsRequestPayload']\nexport type SchemaTrackImages = components['schemas']['TrackImages']\nexport type SchemaGetTracksRequestPayload = components['schemas']['GetTracksRequestPayload']\nexport type SchemaJsonApiErrorSource = components['schemas']['JsonApiErrorSource']\nexport type SchemaJsonApiError = components['schemas']['JsonApiError']\nexport type SchemaJsonApiErrorDocument = components['schemas']['JsonApiErrorDocument']\nexport type SchemaTrackAttachment = components['schemas']['TrackAttachment']\nexport type SchemaTrackListItemAttributes = components['schemas']['TrackListItemAttributes']\nexport type SchemaArtistRelationship = components['schemas']['ArtistRelationship']\nexport type SchemaArtistsRelationship = components['schemas']['ArtistsRelationship']\nexport type SchemaTrackRelationships = components['schemas']['TrackRelationships']\nexport type SchemaTrackListItemResource = components['schemas']['TrackListItemResource']\nexport type SchemaJsonApiMetaWithPagingAndCursor =\n  components['schemas']['JsonApiMetaWithPagingAndCursor']\nexport type SchemaOmitTypeClass = components['schemas']['OmitTypeClass']\nexport type SchemaIncludedArtistOutput = components['schemas']['IncludedArtistOutput']\nexport type SchemaGetTrackListOutput = components['schemas']['GetTrackListOutput']\nexport type SchemaTrackListItemAttributesForPlaylist =\n  components['schemas']['TrackListItemAttributesForPlaylist']\nexport type SchemaTrackListItemResourceForPlaylist =\n  components['schemas']['TrackListItemResourceForPlaylist']\nexport type SchemaJsonApiMeta = components['schemas']['JsonApiMeta']\nexport type SchemaGetTracksForPlaylistOutput = components['schemas']['GetTracksForPlaylistOutput']\nexport type SchemaArtistRef = components['schemas']['ArtistRef']\nexport type SchemaTrackDetailsAttributes = components['schemas']['TrackDetailsAttributes']\nexport type SchemaTrackDetailsResource = components['schemas']['TrackDetailsResource']\nexport type SchemaGetTrackDetailsOutput = components['schemas']['GetTrackDetailsOutput']\nexport type SchemaReactionOutput = components['schemas']['ReactionOutput']\nexport type SchemaGetPlaylistsRequestPayload = components['schemas']['GetPlaylistsRequestPayload']\nexport type SchemaJsonApiMetaWithPaging = components['schemas']['JsonApiMetaWithPaging']\nexport type SchemaGetPlaylistsOutput = components['schemas']['GetPlaylistsOutput']\nexport type SchemaReorderTracksRequestPayload = components['schemas']['ReorderTracksRequestPayload']\nexport type SchemaUpdateTrackRequestPayload = components['schemas']['UpdateTrackRequestPayload']\nexport type SchemaAddTrackToPlaylistRequestPayload =\n  components['schemas']['AddTrackToPlaylistRequestPayload']\nexport type SchemaCreateArtistRequestPayload = components['schemas']['CreateArtistRequestPayload']\nexport type SchemaLoginRequestPayload = components['schemas']['LoginRequestPayload']\nexport type SchemaRefreshOutput = components['schemas']['RefreshOutput']\nexport type SchemaBadRequestException = components['schemas']['BadRequestException']\nexport type SchemaUnauthorizedException = components['schemas']['UnauthorizedException']\nexport type SchemaRefreshRequestPayload = components['schemas']['RefreshRequestPayload']\nexport type SchemaLogoutRequestPayload = components['schemas']['LogoutRequestPayload']\nexport type SchemaGetMeOutput = components['schemas']['GetMeOutput']\nexport type SchemaCreateTagRequestPayload = components['schemas']['CreateTagRequestPayload']\nexport type SchemaBinaryFile = components['schemas']['BinaryFile']\nexport type $defs = Record<string, never>\nexport interface operations {\n  PlaylistsController_getMyPlaylists: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of playlists retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMyPlaylistsOutput']\n        }\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylists: {\n    parameters: {\n      query?: {\n        /** @description Page number for pagination (starting from 1) */\n        pageNumber?: number\n        /** @description Page size for pagination (between 1 and 20) */\n        pageSize?: number\n        /** @description Search term for filtering playlists by name */\n        search?: string\n        /** @description Field by which to sort playlists */\n        sortBy?: PathsPlaylistsGetParametersQuerySortBy\n        /** @description Sort direction (ascending or descending) */\n        sortDirection?: PathsPlaylistsGetParametersQuerySortDirection\n        /** @description Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n        tagsIds?: string[]\n        /** @description Filter by user ID (playlist creator’s ID) */\n        userId?: string\n        /** @description Filter by track ID – only playlists containing this track will be returned */\n        trackId?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: JSON:API list of playlists with pagination */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistsOutput']\n        }\n      }\n    }\n  }\n  PlaylistsController_createPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Playlist created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Forbidden: Playlist creation limit exceeded */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylistById: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the playlist */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Playlist retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Not Found: Playlist with the given ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_updatePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Playlist updated successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Bad Request: Validation error (e.g., tag limit exceeded) */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: You do not have permission to update this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deletePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Playlist deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Insufficient permissions to delete this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_reorderPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderPlaylistsRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Playlist order updated successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist or putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_uploadMainImage: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @description Maximum size 1 MB; minimum height 500px; image must be square */\n          file: components['schemas']['BinaryFile']\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Cover uploaded successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['TrackImages']\n        }\n      }\n      /** @description Bad Request: Invalid image format or dimensions */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No permission to upload cover for this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Cover deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Removing another user’s playlist cover is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getAllTracks: {\n    parameters: {\n      query?: {\n        /** @description Page number for pagination (starting from 1) */\n        pageNumber?: number\n        /** @description Page size for pagination (between 1 and 20) */\n        pageSize?: number\n        /** @description Search term for filtering playlists by name */\n        search?: string\n        /** @description Field by which to sort tracks */\n        sortBy?: PathsPlaylistsTracksGetParametersQuerySortBy\n        /** @description Sort direction (ascending or descending) */\n        sortDirection?: PathsPlaylistsGetParametersQuerySortDirection\n        /** @description Filter by tag IDs (multiple values allowed) */\n        tagsIds?: string[]\n        /** @description Filter by artist IDs (multiple values allowed) */\n        artistsIds?: string[]\n        /** @description Filter by user ID (track creator's ID) */\n        userId?: string\n        /** @description If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n        includeDrafts?: boolean\n        /** @description Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination. */\n        paginationType?: PathsPlaylistsTracksGetParametersQueryPaginationType\n        /** @description Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\". */\n        cursor?: string | null\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Paginated list of tracks */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackListOutput']\n        }\n      }\n      /** @description Bad Request: invalid query parameters */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['JsonApiErrorDocument']\n        }\n      }\n    }\n  }\n  TracksPublicController_getPlaylistTracks: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the playlist to retrieve tracks for */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of tracks in the playlist */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTracksForPlaylistOutput']\n        }\n      }\n      /** @description Not Found: Playlist with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getTrackDetails: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the track to retrieve details for */\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Track details with attachments */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description Not Found: Track with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_updateTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdateTrackRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Track updated successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description Bad Request: Tag or artist limit exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Editing another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track or playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCompletely: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track permanently deleted */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Deleting another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_likeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: User reaction recorded and counters updated */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid track ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_dislikeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: User reaction recorded and counters updated */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid track ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_removeTrackReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Reaction removed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_likePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: Like recorded successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid playlist ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_dislikePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: Dislike recorded successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid playlist ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_removePlaylistReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Reaction removed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_reorderTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderTracksRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Track order updated successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Bad Request: Cannot place a track after itself */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track or putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_addTrackToPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['AddTrackToPlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Track added to the playlist successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist or track limit exceeded (max 10 tracks) */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_unbindTrackFromPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track removed from the playlist */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_publishTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track published successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Publishing another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Track is already published */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the track for which the cover is being uploaded */\n        trackId: string\n      }\n      cookie?: never\n    }\n    /** @description Image file:<br/>\n     *             • Field name — <code>cover</code><br/>\n     *             • Allowed MIME types — <code>image/jpeg</code>, <code>image/png</code>, <code>image/gif</code><br/>\n     *             • Maximum size — <code>100 KB</code> */\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** Format: binary */\n          cover: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Cover uploaded successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['TrackImages']\n        }\n      }\n      /** @description Bad Request: Invalid file or size exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Cannot upload a cover for another user’s track */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Cover deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Removing another user's track cover is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackMp3: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @example My cool track */\n          title: string\n          /** Format: binary */\n          file: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Track created successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description Bad Request: Invalid file format or file size exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Internal Server Error: Error saving file or track */\n      500: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_createArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateArtistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Artist created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ArtistRef']\n        }\n      }\n      /** @description Bad Request: Validation error or invalid input */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Limit of 100 artists per user reached */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Artist with the given name already exists */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_searchArtist: {\n    parameters: {\n      query: {\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of artists matching the search */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ArtistRef'][]\n        }\n      }\n    }\n  }\n  ArtistsController_deleteArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Artist deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Artist is attached to tracks or was created by another user */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Artist with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_OauthRedirect: {\n    parameters: {\n      query: {\n        /** @description The callback URL to redirect after grand access,\n         *          https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid */\n        callbackUrl: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Redirect executed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_login: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LoginRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Token pair retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Bad Request: Invalid request format or required parameters are missing */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['BadRequestException']\n        }\n      }\n      /** @description Unauthorized: Code is invalid, expired, missing, or redirectUri does not match */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_refresh: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['RefreshRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Token pair refreshed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Unauthorized: Refresh token is invalid, expired, or missing */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_logout: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LogoutRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Refresh token deactivated; access token remains valid. */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_getMe: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Successfully retrieved user information */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMeOutput']\n        }\n      }\n      /** @description Unauthorized: access token is missing or invalid */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_createTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateTagRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Tag created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['TagRef']\n        }\n      }\n      /** @description Bad Request: Validation error */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Limit of 100 tags per user reached */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Tag with the given name already exists */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_searchTags: {\n    parameters: {\n      query: {\n        /** @description Substring to search tags by (using normalized name) */\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of matching tags */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['TagRef'][]\n        }\n      }\n      /** @description Bad Request: Invalid search query */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_deleteTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the tag to delete */\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Tag deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Tag was created by another user or is attached to tracks or playlists */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Tag with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n}\nexport enum PathsPlaylistsGetParametersQuerySortBy {\n  addedAt = 'addedAt',\n  likesCount = 'likesCount',\n}\nexport enum PathsPlaylistsGetParametersQuerySortDirection {\n  asc = 'asc',\n  desc = 'desc',\n}\nexport enum PathsPlaylistsTracksGetParametersQuerySortBy {\n  publishedAt = 'publishedAt',\n  likesCount = 'likesCount',\n}\nexport enum PathsPlaylistsTracksGetParametersQueryPaginationType {\n  offset = 'offset',\n  cursor = 'cursor',\n}\nexport enum ImageSizeType {\n  original = 'original',\n  thumbnail = 'thumbnail',\n  medium = 'medium',\n}\nexport enum ReactionValue {\n  Value0 = 0,\n  Value1 = 1,\n  ValueMinus1 = -1,\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/api/utils/json-api-error.ts",
    "content": "export interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport type ExtractError<T> = T extends { error?: infer E } ? E : unknown\n\n/* --- типы ошибок, совпадающие с фильтром -------------------------------- */\nexport interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport function isJsonApiErrorDocument(error: unknown): error is JsonApiErrorDocument {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    // @ts-expect-error type no matter\n    Array.isArray(error.errors)\n  )\n}\n\nexport function parseJsonApiErrors(errorDoc: JsonApiErrorDocument): {\n  fieldErrors: Record<string, string>\n  globalErrors: string[]\n} {\n  const fieldErrors: Record<string, string> = {}\n  const globalErrors: string[] = []\n\n  for (const err of errorDoc.errors) {\n    const msg = err.detail ?? err.title ?? 'Unknown error'\n    const ptr = err.source?.pointer\n    if (ptr) {\n      // убираем префикс JSON:API\n      const field = ptr.replace(/^\\/data\\/attributes\\//, '')\n      fieldErrors[field] = msg\n    } else {\n      globalErrors.push(msg)\n    }\n  }\n\n  return { fieldErrors, globalErrors }\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/api/utils/request-wrapper.ts",
    "content": "// types/api.ts\n\nimport { type ExtractError } from './json-api-error.ts'\n\n//-----------------------------------------------------------------------------\n// utils/requestWrapper.ts\n//-----------------------------------------------------------------------------\n// «Умный» обёртчик: Infers Data и Error из P,\n// возвращает Promise<Data>, а в случае ошибки — throw Error\nexport type ExtractData<T> = T extends { data?: infer D } ? NonNullable<D> : never\n\nexport async function requestWrapper<P extends Promise<{ data?: unknown; error?: unknown }>>(\n  promise: P\n): Promise<ExtractData<Awaited<P>>> {\n  const res = (await promise) as Awaited<P>\n  if ((res as { error?: unknown }).error) {\n    // здесь E = ExtractError<Awaited<P>>\n    throw (res as { error: ExtractError<Awaited<P>> }).error\n  }\n  return (res as { data: ExtractData<Awaited<P>> }).data\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/AudioPlayer/AudioPlayer.module.css",
    "content": ".player {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  width: 100%;\n  min-height: 64px;\n\n  background: var(--color-bg-primary);\n}\n\n.trackInfo {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  min-width: 200px;\n}\n\n.cover {\n  width: 112px;\n  height: 112px;\n  border-radius: 4px;\n  background: var(--color-bg-card);\n}\n\n.cover img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.info {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.playerControls {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 8px;\n  align-items: center;\n}\n\n.controls {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n}\n\n.playPauseButton {\n  width: 48px;\n  height: 48px;\n}\n\n.active {\n  color: var(--color-accent);\n}\n\n.iconButton.active:hover,\n.iconButton.active:focus {\n  color: var(--color-accent);\n}\n\n.progressBar {\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  max-width: 632px;\n}\n\n.time {\n  min-width: 36px;\n  font-size: var(--font-size-xs);\n  color: var(--color-text-secondary);\n  text-align: center;\n}\n\n.progress {\n  cursor: pointer;\n\n  height: 5px;\n  border: none;\n  border-radius: 4px;\n\n  accent-color: var(--color-text-primary);\n}\n\n.trackProgress {\n  width: 100%;\n  max-width: 550px;\n}\n\n.volumeColumn {\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n\n  min-width: 160px;\n  padding-right: 32px;\n}\n\n.volumeProgress {\n  width: 119px;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/AudioPlayer/AudioPlayer.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { AudioPlayer } from './AudioPlayer.tsx'\n\nconst meta = {\n  title: 'Components/Player',\n  component: AudioPlayer,\n  parameters: {},\n  args: {},\n} satisfies Meta<typeof AudioPlayer>\n\nexport default meta\n\nconst demoTrack = {\n  src: 'https://cdn.uppbeat.io/audio-files/c636d7c86452449b1203fc0bded83e29/4358717fc9da477a52fb18a6cbd3afcc/d154b5ce5ff1a05ae8115a3c678062e8/STREAMING-dreamland-matrika-main-version-31140-02-25.mp3',\n  cover: 'https://unsplash.it/112/112',\n  title: 'Play It Safe',\n  artist: 'Julia Wolf',\n}\n\nexport const Basic = {\n  render: () => {\n    const [isPlaying, setIsPlaying] = useState(false)\n    const [isShuffle, setIsShuffle] = useState(false)\n    const [isRepeat, setIsRepeat] = useState(false)\n\n    const [track] = useState(demoTrack)\n    return (\n      <AudioPlayer\n        {...track}\n        isPlaying={isPlaying}\n        setIsPlaying={setIsPlaying}\n        onNext={() => {}}\n        onPrevious={() => {}}\n        isShuffle={isShuffle}\n        isRepeat={isRepeat}\n        onShuffle={() => setIsShuffle(!isShuffle)}\n        onRepeat={() => setIsRepeat(!isRepeat)}\n      />\n    )\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/AudioPlayer/AudioPlayer.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ComponentProps, useRef, useState } from 'react'\n\nimport {\n  PauseIcon,\n  PlayIcon,\n  RepeatIcon,\n  ShuffleIcon,\n  SkipNextIcon,\n  SkipPreviousIcon,\n  VolumeIcon,\n  VolumeMuteIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './AudioPlayer.module.css'\n\nexport type PlayerProps = {\n  src: string\n  cover: string\n  title: string\n  artist: string\n  isPlaying: boolean\n  setIsPlaying: (isPlaying: boolean) => void\n  onNext: () => void\n  onPrevious: () => void\n  isShuffle: boolean\n  isRepeat: boolean\n  onShuffle: () => void\n  onRepeat: () => void\n} & ComponentProps<'div'>\n\nexport const AudioPlayer = ({\n  src,\n  cover,\n  title,\n  artist,\n  isPlaying,\n  setIsPlaying,\n  onNext,\n  onPrevious,\n  isShuffle,\n  isRepeat,\n  onShuffle,\n  onRepeat,\n  className,\n  ...props\n}: PlayerProps) => {\n  const audioRef = useRef<HTMLAudioElement | null>(null)\n  const [currentTime, setCurrentTime] = useState(0)\n  const [volume, setVolume] = useState(1)\n  const [duration, setDuration] = useState(0)\n\n  const handlePlayPause = () => {\n    const audio = audioRef.current\n    if (!audio) return\n\n    if (isPlaying) {\n      audio.pause()\n    } else {\n      audio.play().catch((e) => {\n        console.error('Audio play error:', e)\n      })\n    }\n\n    setIsPlaying(!isPlaying)\n  }\n\n  const handleChangeTime = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const time = Number(e.target.value)\n    setCurrentTime(time)\n    if (audioRef.current) {\n      audioRef.current.currentTime = time\n    }\n  }\n\n  const handleVolume = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newVolume = Number(e.target.value)\n    setVolume(newVolume)\n    if (audioRef.current) {\n      audioRef.current.volume = newVolume\n    }\n  }\n\n  const handleVolumeMute = () => {\n    const newVolume = volume > 0 ? 0 : 1\n    setVolume(newVolume)\n    if (audioRef.current) {\n      audioRef.current.volume = newVolume\n    }\n  }\n\n  return (\n    <div className={clsx(s.player, className)} {...props}>\n      <audio\n        ref={audioRef}\n        src={src}\n        onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}\n        onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}\n      />\n\n      <div className={s.trackInfo}>\n        <div className={s.cover}>\n          <img src={cover} alt=\"cover\" />\n        </div>\n        <div className={s.info}>\n          <Typography variant=\"body1\" as=\"h3\">\n            {title}\n          </Typography>\n          <Typography variant=\"body2\" as=\"p\">\n            {artist}\n          </Typography>\n        </div>\n      </div>\n\n      <div className={s.playerControls}>\n        <div className={s.controls}>\n          <IconButton onClick={onShuffle} className={clsx(s.iconButton, isShuffle && s.active)}>\n            <ShuffleIcon />\n          </IconButton>\n          <IconButton onClick={onPrevious}>\n            <SkipPreviousIcon />\n          </IconButton>\n          <IconButton className={s.playPauseButton} onClick={handlePlayPause}>\n            {isPlaying ? <PauseIcon /> : <PlayIcon />}\n          </IconButton>\n          <IconButton onClick={onNext}>\n            <SkipNextIcon />\n          </IconButton>\n          <IconButton onClick={onRepeat} className={clsx(s.iconButton, isRepeat && s.active)}>\n            <RepeatIcon />\n          </IconButton>\n        </div>\n\n        <div className={s.progressBar}>\n          <span className={s.time}>{format(currentTime)}</span>\n          <input\n            type=\"range\"\n            min={0}\n            max={duration}\n            value={currentTime}\n            onChange={handleChangeTime}\n            className={clsx(s.progress, s.trackProgress)}\n          />\n          <span className={s.time}>{format(duration)}</span>\n        </div>\n      </div>\n\n      <div className={s.volumeColumn}>\n        <IconButton onClick={handleVolumeMute}>\n          {volume > 0 ? <VolumeIcon /> : <VolumeMuteIcon />}\n        </IconButton>\n        <input\n          type=\"range\"\n          min={0}\n          max={1}\n          step={0.01}\n          value={volume}\n          onChange={handleVolume}\n          className={clsx(s.progress, s.volumeProgress)}\n        />\n      </div>\n    </div>\n  )\n}\n\nconst format = (sec: number) => {\n  const m = Math.floor(sec / 60)\n  const s = Math.floor(sec % 60)\n  return `${m}:${s.toString().padStart(2, '0')}`\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/AudioPlayer/index.ts",
    "content": "export * from './AudioPlayer.tsx'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Autocomplete/Autocomplete.module.css",
    "content": ".container {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.labelError {\n  color: var(--color-text-error);\n}\n\n.inputWrapper {\n  position: relative;\n\n  display: flex;\n  flex-wrap: wrap;\n  gap: 6px;\n  align-items: center;\n\n  min-height: 48px;\n  padding: 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.inputWrapper:hover:not(.disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.inputWrapper.focused {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-primary);\n}\n\n.inputWrapper.error {\n  border-color: var(--color-text-error);\n}\n\n.inputWrapper.disabled {\n  cursor: not-allowed;\n  background-color: var(--color-disabled);\n}\n\n.tag {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n\n  padding: 2px 6px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 16px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.inputContainer {\n  position: relative;\n\n  display: flex;\n  flex: 1;\n  align-items: center;\n\n  min-width: 120px;\n}\n\n.searchIcon {\n  pointer-events: none;\n\n  position: absolute;\n  z-index: 1;\n  left: 4px;\n\n  width: 16px;\n  height: 16px;\n\n  color: var(--color-text-secondary);\n\n  transition: color 200ms ease;\n}\n\n.input {\n  width: 100%;\n  padding: 4px 8px 4px 24px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background: transparent;\n  outline: none;\n\n  transition: all 200ms ease;\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.dropdownIcon {\n  cursor: pointer;\n\n  width: 20px;\n  height: 20px;\n  margin-left: 4px;\n\n  color: var(--color-text-secondary);\n\n  transition: transform 200ms ease;\n}\n\n.dropdownIcon:hover {\n  color: var(--color-text-primary);\n}\n\n.dropdownIconOpen {\n  transform: rotate(180deg);\n}\n\n.dropdown {\n  position: absolute;\n  z-index: 50;\n  top: 100%;\n  left: 0;\n\n  overflow-y: auto;\n\n  width: 100%;\n  max-height: 200px;\n  margin-top: 4px;\n  padding: 4px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n\n  animation: dropdown-show 200ms ease-out;\n}\n\n.option {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n\n  padding: 8px 12px;\n  border-radius: 4px;\n\n  transition: all 200ms ease;\n}\n\n.option:hover:not(.optionDisabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.optionFocused:not(.optionDisabled) {\n  color: var(--color-bg-primary);\n  background-color: var(--color-accent);\n}\n\n.optionDisabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.noResults {\n  padding: 12px;\n  text-align: center;\n}\n\n.noResultsText {\n  color: var(--color-text-secondary);\n}\n\n.errorMessage {\n  margin-top: 4px;\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.counter {\n  margin-top: 4px;\n  color: var(--color-text-secondary);\n}\n\n/* Animations */\n@keyframes dropdown-show {\n  from {\n    transform: translateY(-4px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Autocomplete/Autocomplete.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from '../Dialog'\nimport { Typography } from '../Typography'\nimport { Autocomplete, type AutocompleteOption } from './Autocomplete'\n\nconst meta = {\n  title: 'Components/Autocomplete',\n  component: Autocomplete,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Autocomplete>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n// Sample data\nconst programmingLanguages: AutocompleteOption[] = [\n  { value: 'javascript', label: 'JavaScript' },\n  { value: 'typescript', label: 'TypeScript' },\n  { value: 'python', label: 'Python' },\n  { value: 'java', label: 'Java' },\n  { value: 'cpp', label: 'C++' },\n  { value: 'csharp', label: 'C#' },\n  { value: 'php', label: 'PHP' },\n  { value: 'ruby', label: 'Ruby' },\n  { value: 'go', label: 'Go' },\n  { value: 'rust', label: 'Rust' },\n  { value: 'kotlin', label: 'Kotlin' },\n  { value: 'swift', label: 'Swift' },\n]\n\nconst musicGenres: AutocompleteOption[] = [\n  { value: 'rock', label: 'Rock' },\n  { value: 'pop', label: 'Pop' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hiphop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n  { value: 'blues', label: 'Blues' },\n  { value: 'reggae', label: 'Reggae' },\n  { value: 'folk', label: 'Folk' },\n  { value: 'metal', label: 'Metal' },\n  { value: 'indie', label: 'Indie' },\n]\n\nconst skills: AutocompleteOption[] = [\n  { value: 'frontend', label: 'Frontend Development' },\n  { value: 'backend', label: 'Backend Development' },\n  { value: 'fullstack', label: 'Full Stack Development' },\n  { value: 'mobile', label: 'Mobile Development' },\n  { value: 'devops', label: 'DevOps' },\n  { value: 'testing', label: 'Testing & QA' },\n  { value: 'design', label: 'UI/UX Design' },\n  { value: 'pm', label: 'Project Management', disabled: true },\n  { value: 'data', label: 'Data Science' },\n  { value: 'ml', label: 'Machine Learning' },\n]\n\nexport const Basic = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Programming Languages\"\n          placeholder=\"Search and select languages...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (max 3)\"\n          placeholder=\"Choose up to 3 genres...\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          maxTags={3}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithPreselected = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>(['javascript', 'typescript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Your Skills\"\n          placeholder=\"Add more skills...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithDisabledOptions = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Skills & Roles\"\n          placeholder=\"Select your skills...\"\n          options={skills}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>(['rock', 'jazz'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (disabled)\"\n          placeholder=\"Cannot select\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          disabled\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithError = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Required Skills\"\n          placeholder=\"Select at least one skill...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          errorMessage=\"Please select at least one programming language\"\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendSkills, setFrontendSkills] = useState<string[]>(['javascript'])\n    const [backendSkills, setBackendSkills] = useState<string[]>([])\n    const [genres, setGenres] = useState<string[]>([])\n\n    const frontendOptions: AutocompleteOption[] = [\n      { value: 'html', label: 'HTML' },\n      { value: 'css', label: 'CSS' },\n      { value: 'javascript', label: 'JavaScript' },\n      { value: 'typescript', label: 'TypeScript' },\n      { value: 'react', label: 'React' },\n      { value: 'vue', label: 'Vue.js' },\n      { value: 'angular', label: 'Angular' },\n      { value: 'svelte', label: 'Svelte' },\n    ]\n\n    const backendOptions: AutocompleteOption[] = [\n      { value: 'nodejs', label: 'Node.js' },\n      { value: 'python', label: 'Python' },\n      { value: 'java', label: 'Java' },\n      { value: 'csharp', label: 'C#' },\n      { value: 'php', label: 'PHP' },\n      { value: 'ruby', label: 'Ruby' },\n      { value: 'go', label: 'Go' },\n      { value: 'rust', label: 'Rust' },\n    ]\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Developer Profile Setup\n          </Typography>\n        </div>\n\n        <Autocomplete\n          label=\"Frontend Technologies\"\n          placeholder=\"Select frontend skills...\"\n          options={frontendOptions}\n          value={frontendSkills}\n          onChange={setFrontendSkills}\n          maxTags={5}\n        />\n\n        <Autocomplete\n          label=\"Backend Technologies\"\n          placeholder=\"Select backend skills...\"\n          options={backendOptions}\n          value={backendSkills}\n          onChange={setBackendSkills}\n          maxTags={4}\n        />\n\n        <Autocomplete\n          label=\"Favorite Music Genres\"\n          placeholder=\"What music do you like?\"\n          options={musicGenres}\n          value={genres}\n          onChange={setGenres}\n          maxTags={6}\n        />\n\n        <Card style={{ padding: '16px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Profile Summary\n          </Typography>\n\n          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>\n            <Typography variant=\"body2\">\n              <strong>Frontend:</strong>{' '}\n              {frontendSkills.length > 0 ? frontendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Backend:</strong>{' '}\n              {backendSkills.length > 0 ? backendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Music:</strong> {genres.length > 0 ? genres.join(', ') : 'None'}\n            </Typography>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => {\n    const [state1, setState1] = useState<string[]>([])\n    const [state2, setState2] = useState<string[]>(['rock', 'jazz'])\n    const [state3, setState3] = useState<string[]>([])\n    const [state4, setState4] = useState<string[]>(['javascript'])\n\n    return (\n      <div\n        style={{\n          width: '600px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '32px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Empty State\n          </Typography>\n          <Autocomplete\n            label=\"Programming Languages\"\n            placeholder=\"Start typing to search...\"\n            options={programmingLanguages}\n            value={state1}\n            onChange={setState1}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Selected Values\n          </Typography>\n          <Autocomplete\n            label=\"Music Genres\"\n            placeholder=\"Add more genres...\"\n            options={musicGenres}\n            value={state2}\n            onChange={setState2}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Error\n          </Typography>\n          <Autocomplete\n            label=\"Required Field\"\n            placeholder=\"This field is required\"\n            options={programmingLanguages}\n            value={state3}\n            onChange={setState3}\n            errorMessage=\"Please select at least one option\"\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Disabled State\n          </Typography>\n          <Autocomplete\n            label=\"Locked Selection\"\n            placeholder=\"Cannot modify\"\n            options={programmingLanguages}\n            value={state4}\n            onChange={setState4}\n            disabled\n          />\n        </div>\n      </div>\n    )\n  },\n}\n\nexport const InDialog = {\n  render: () => {\n    const [isOpen, setIsOpen] = useState(false)\n    const [selectedSkills, setSelectedSkills] = useState<string[]>([])\n    const [selectedGenres, setSelectedGenres] = useState<string[]>(['rock'])\n\n    const handleSubmit = () => {\n      console.log('Selected skills:', selectedSkills)\n      console.log('Selected genres:', selectedGenres)\n      setIsOpen(false)\n    }\n\n    const handleReset = () => {\n      setSelectedSkills([])\n      setSelectedGenres([])\n    }\n\n    return (\n      <>\n        <Button onClick={() => setIsOpen(true)}>Open Profile Settings</Button>\n\n        <Dialog open={isOpen} onClose={() => setIsOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Edit Your Profile</Typography>\n            <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n              Update your skills and music preferences\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '24px',\n                minWidth: '400px',\n              }}>\n              <Autocomplete\n                label=\"Technical Skills\"\n                placeholder=\"Search and select your skills...\"\n                options={skills}\n                value={selectedSkills}\n                onChange={setSelectedSkills}\n                maxTags={8}\n              />\n\n              <Autocomplete\n                label=\"Favorite Music Genres\"\n                placeholder=\"What music do you enjoy?\"\n                options={musicGenres}\n                value={selectedGenres}\n                onChange={setSelectedGenres}\n                maxTags={5}\n              />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset All\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setIsOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={handleSubmit}>\n              Save Profile\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Autocomplete/Autocomplete.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\n\nimport { useGetId } from '@/shared/hooks'\nimport { ArrowDownIcon, DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './Autocomplete.module.css'\n\nexport type AutocompleteOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type AutocompleteProps = {\n  label?: ReactNode\n  placeholder?: string\n  options: AutocompleteOption[]\n  value: string[]\n  onChange: (value: string[]) => void\n  disabled?: boolean\n  maxTags?: number\n  errorMessage?: string\n  className?: string\n} & Omit<ComponentProps<'div'>, 'onChange'>\n\nexport const Autocomplete = ({\n  label,\n  placeholder = 'Search and select...',\n  options,\n  value,\n  onChange,\n  disabled = false,\n  maxTags,\n  errorMessage,\n  className,\n  ...props\n}: AutocompleteProps) => {\n  const [isOpen, setIsOpen] = useState(false)\n  const [searchTerm, setSearchTerm] = useState('')\n  const [focusedIndex, setFocusedIndex] = useState(-1)\n\n  // For detecting clicks outside component to close dropdown\n  const containerRef = useRef<HTMLDivElement>(null)\n  // For programmatic focus management (Escape key, focus after selection)\n  const inputRef = useRef<HTMLInputElement>(null)\n\n  const id = useGetId(props.id)\n\n  const filteredOptions = options.filter(\n    (option) =>\n      option.label.toLowerCase().includes(searchTerm.toLowerCase()) && !value.includes(option.value)\n  )\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n  const showError = Boolean(errorMessage)\n\n  // Close dropdown on outside click\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n        setIsOpen(false)\n        setFocusedIndex(-1)\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen])\n\n  // Handle keyboard navigation\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (disabled) return\n\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault()\n        if (!isOpen) {\n          setIsOpen(true)\n          setFocusedIndex(0)\n        } else {\n          setFocusedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : prev))\n        }\n        break\n\n      case 'ArrowUp':\n        e.preventDefault()\n        setFocusedIndex((prev) => (prev > 0 ? prev - 1 : 0))\n        break\n\n      case 'Enter':\n        e.preventDefault()\n        if (isOpen && focusedIndex >= 0 && filteredOptions[focusedIndex]) {\n          selectOption(filteredOptions[focusedIndex])\n        }\n        break\n\n      case 'Escape':\n        e.preventDefault()\n        setIsOpen(false)\n        setFocusedIndex(-1)\n        inputRef.current?.blur()\n        break\n\n      case 'Backspace':\n        if (!searchTerm && value.length > 0) {\n          removeTag(value[value.length - 1])\n        }\n        break\n    }\n  }\n\n  const selectOption = (option: AutocompleteOption) => {\n    if (option.disabled || isMaxTagsReached) return\n\n    onChange([...value, option.value])\n    setSearchTerm('')\n    setFocusedIndex(-1)\n    inputRef.current?.focus()\n  }\n\n  const removeTag = (tagValue: string) => {\n    onChange(value.filter((v) => v !== tagValue))\n  }\n\n  const handleInputFocus = () => {\n    if (!disabled) {\n      setIsOpen(true)\n    }\n  }\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setSearchTerm(e.target.value)\n    setIsOpen(true)\n    setFocusedIndex(-1)\n  }\n\n  const selectedOptions = options.filter((option) => value.includes(option.value))\n\n  return (\n    <div className={clsx(s.container, className)} ref={containerRef} {...props}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          className={clsx(s.label, showError && s.labelError)}\n          as=\"label\"\n          htmlFor={id}>\n          {label}\n        </Typography>\n      )}\n\n      <div\n        className={clsx(\n          s.inputWrapper,\n          isOpen && s.focused,\n          showError && s.error,\n          disabled && s.disabled\n        )}>\n        {/* Selected tags */}\n        {selectedOptions.map((option) => (\n          <div key={option.value} className={s.tag}>\n            <Typography variant=\"body2\" className={s.tagText} as=\"label\">\n              {option.label}\n            </Typography>\n            {!disabled && (\n              <IconButton\n                onClick={() => removeTag(option.value)}\n                className={s.deleteButton}\n                aria-label={`Remove ${option.label}`}\n                type=\"button\"\n                tabIndex={-1}>\n                <DeleteIcon />\n              </IconButton>\n            )}\n          </div>\n        ))}\n\n        {/* Search input */}\n        <div className={s.inputContainer}>\n          <input\n            id={id}\n            ref={inputRef}\n            type=\"text\"\n            className={s.input}\n            value={searchTerm}\n            onChange={handleInputChange}\n            onFocus={handleInputFocus}\n            onKeyDown={handleKeyDown}\n            placeholder={value.length === 0 ? placeholder : ''}\n            disabled={disabled || isMaxTagsReached}\n            autoComplete=\"off\"\n          />\n        </div>\n\n        {/* Dropdown arrow */}\n        <ArrowDownIcon\n          className={clsx(s.dropdownIcon, isOpen && s.dropdownIconOpen)}\n          onClick={() => !disabled && setIsOpen(!isOpen)}\n        />\n      </div>\n\n      {/* Dropdown */}\n      {isOpen && !disabled && (\n        <div className={s.dropdown}>\n          {filteredOptions.length > 0 ? (\n            filteredOptions.map((option, index) => (\n              <div\n                key={option.value}\n                className={clsx(\n                  s.option,\n                  index === focusedIndex && s.optionFocused,\n                  option.disabled && s.optionDisabled\n                )}\n                onClick={() => !option.disabled && selectOption(option)}\n                onMouseEnter={() => setFocusedIndex(index)}>\n                <Typography variant=\"body2\">{option.label}</Typography>\n              </div>\n            ))\n          ) : (\n            <div className={s.noResults}>\n              <Typography variant=\"body2\" className={s.noResultsText}>\n                {searchTerm ? 'No options found' : 'All options selected'}\n              </Typography>\n            </div>\n          )}\n        </div>\n      )}\n\n      {/* Error message */}\n      {showError && (\n        <Typography variant=\"error\" className={s.errorMessage}>\n          {errorMessage}\n        </Typography>\n      )}\n\n      {/* Tags counter */}\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} selected\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Autocomplete/index.ts",
    "content": "export * from './Autocomplete'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Button/Button.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: inline-flex;\n  gap: 4px;\n  align-items: center;\n  justify-content: center;\n\n  height: 40px;\n  padding: 8px 16px;\n  border-radius: 45px;\n\n  font-size: var(--font-size-s);\n  font-weight: 600;\n  color: var(--color-text-primary);\n\n  transition: opacity 200ms;\n}\n\n.button:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.button:disabled {\n  cursor: initial;\n  background-color: var(--color-disabled);\n}\n\n.button:hover:not(:disabled),\n.button:focus:not(:disabled) {\n  opacity: 0.8;\n}\n\n.primary {\n  background-color: var(--color-accent);\n}\n\n.secondary {\n  background-color: var(--color-bg-interactive-secondary);\n}\n\n.fullWidth {\n  width: 100%;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Button/Button.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Button } from './Button'\n\nconst meta = {\n  title: 'Components/Button',\n  component: Button,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Button>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllButtons: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '24px',\n        flexDirection: 'column',\n        alignItems: 'center',\n        width: '250px',\n      }}>\n      <Button variant=\"primary\">Primary</Button>\n      <Button variant=\"secondary\">Secondary</Button>\n      <Button fullWidth>Full Width</Button>\n      <Button disabled>Disabled</Button>\n      <Button variant=\"primary\" as=\"p\" href=\"https://it-incubator.io/\" target=\"_blank\">\n        Link\n      </Button>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Button/Button.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Button.module.css'\n\nexport type ButtonVariant = 'primary' | 'secondary'\n\nexport type ButtonProps<T extends ElementType = 'button'> = {\n  as?: T\n  fullWidth?: boolean\n  variant?: ButtonVariant\n} & ComponentProps<T>\n\nexport const Button = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  className,\n  fullWidth = false,\n  variant = 'primary',\n  ...props\n}: ButtonProps<T>) => {\n  const classNames = clsx(s.button, s[variant], fullWidth && s.fullWidth, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Button/index.ts",
    "content": "export * from './Button'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Card/Card.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  padding: 8px;\n  background: var(--color-bg-card);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Card/Card.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from '../Typography'\nimport { Card } from './Card'\n\nconst meta = {\n  title: 'Components/Card',\n  component: Card,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Card>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  render: () => (\n    <Card>\n      <Typography variant=\"h2\">Chill Mix</Typography>\n      <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n        Julia Wolf, Khalid, ayokay and more\n      </Typography>\n    </Card>\n  ),\n}\n\nexport const AsSection: Story = {\n  render: () => (\n    <Card as=\"section\">\n      <Typography variant=\"h3\">Card as section</Typography>\n      <Typography variant=\"caption\">You can use any tag via 'as' prop</Typography>\n    </Card>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Card/Card.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType, ReactNode } from 'react'\n\nimport s from './Card.module.css'\n\nexport type CardProps<T extends ElementType = 'div'> = {\n  as?: T\n  className?: string\n  children?: ReactNode\n} & ComponentProps<T>\n\nexport const Card = <T extends ElementType = 'div'>({\n  as: Component = 'div',\n  className,\n  children,\n  ...props\n}: CardProps<T>) => {\n  return (\n    <Component className={clsx(s.card, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Card/index.ts",
    "content": "export * from './Card'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Dialog/Dialog.module.css",
    "content": ".backdrop {\n  position: fixed;\n  z-index: 1;\n  inset: 0;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  background-color: rgb(0 0 0 / 50%);\n\n  animation: fade-in 200ms ease-out;\n}\n\n.dialog {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n\n  max-width: 745px;\n  max-height: 90vh;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-secondary);\n\n  animation: slide-in 200ms ease-out;\n}\n\n.header {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  padding: 18px 24px;\n}\n\n.closeButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: 16px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.closeButton:hover {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  overflow-y: auto;\n  flex: 1;\n  padding: 20px 24px;\n}\n\n.footer {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 8px;\n  padding: 18px 24px;\n}\n\n/* Animations */\n@keyframes fade-in {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes slide-in {\n  from {\n    transform: translateY(-500px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n/* Responsive */\n@media (width <= 768px) {\n  .dialog {\n    max-width: 95vw;\n    margin: 20px;\n  }\n\n  .header,\n  .content,\n  .footer {\n    padding-right: 16px;\n    padding-left: 16px;\n  }\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Dialog/Dialog.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from './index'\n\nconst meta = {\n  title: 'Components/Dialog',\n  component: Dialog,\n  parameters: {\n    layout: 'centered',\n  },\n} satisfies Meta<typeof Dialog>\n\nexport default meta\n\nexport const BasicDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Open Basic Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Dialog Title</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <Typography variant=\"body1\">\n              This is dialog content. Here can be any content - text, forms, images and much more.\n            </Typography>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Confirm\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const FormDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Form Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Sign in to Spotifun</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '16px',\n                minWidth: '320px',\n              }}>\n              <TextField label=\"Email or username\" placeholder=\"Enter email or username\" />\n              <TextField label=\"Password\" type=\"password\" placeholder=\"Enter password\" />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Continue without signing in\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Sign in with API/HUB\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const WithoutCloseButton = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog without close button</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader showCloseButton={false}>\n            <Typography variant=\"h2\">Millions of songs.</Typography>\n            <Typography variant=\"body1\" style={{ color: 'var(--color-text-secondary)' }}>\n              Free on Musicfun.\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ textAlign: 'center', padding: '20px 0' }}>\n              <div\n                style={{\n                  width: '60px',\n                  height: '60px',\n                  borderRadius: '50%',\n                  backgroundColor: 'var(--color-accent)',\n                  margin: '0 auto 16px',\n                  display: 'flex',\n                  alignItems: 'center',\n                  justifyContent: 'center',\n                  fontSize: '24px',\n                }}>\n                😊\n              </div>\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '12px',\n                width: '100%',\n              }}>\n              <Button variant=\"primary\" fullWidth onClick={() => setOpen(false)}>\n                Sign up with API/HUB\n              </Button>\n              <Button variant=\"secondary\" fullWidth onClick={() => setOpen(false)}>\n                Continue without signing in\n              </Button>\n            </div>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const LongContent = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog with long content</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Long Content</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ maxWidth: '500px' }}>\n              {Array.from({ length: 20 }, (_, i) => (\n                <Typography key={i} variant=\"body2\" style={{ marginBottom: '12px' }}>\n                  This is paragraph number {i + 1}. Lorem ipsum dolor sit amet, consectetur\n                  adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna\n                  aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\n                </Typography>\n              ))}\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Close\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Dialog/Dialog.tsx",
    "content": "import { clsx } from 'clsx'\nimport { createContext, type ReactNode, use, useEffect } from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { IconButton } from '../IconButton'\nimport s from './Dialog.module.css'\n\ntype DialogContextType = {\n  onClose?: () => void\n}\n\nconst DialogContext = createContext<DialogContextType | null>(null)\n\nconst useDialogContext = () => {\n  const context = use(DialogContext)\n  if (!context) {\n    throw new Error('Dialog compound components must be used within Dialog component')\n  }\n  return context\n}\n\nexport type DialogProps = {\n  children: ReactNode\n  open: boolean\n  onClose?: () => void\n  className?: string\n}\n\nexport const Dialog = ({ children, open, onClose, className }: DialogProps) => {\n  const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (e.target === e.currentTarget) {\n      onClose?.()\n    }\n  }\n\n  // Add global keydown handler for ESC key\n  useEffect(() => {\n    if (!open) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose?.()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [open, onClose])\n\n  if (!open) return null\n\n  const dialogContent = (\n    <div className={s.backdrop} onClick={handleBackdropClick} role=\"dialog\" aria-modal=\"true\">\n      <section className={clsx(s.dialog, className)}>\n        <DialogContext value={{ onClose }}>{children}</DialogContext>\n      </section>\n    </div>\n  )\n\n  return createPortal(dialogContent, document.body)\n}\n\n/*\n * DialogHeader\n */\n\nexport type DialogHeaderProps = {\n  children?: ReactNode\n  className?: string\n  showCloseButton?: boolean\n}\n\nexport const DialogHeader = ({\n  children,\n  className,\n  showCloseButton = true,\n}: DialogHeaderProps) => {\n  const { onClose } = useDialogContext()\n\n  return (\n    <header className={clsx(s.header, className)}>\n      <div>{children}</div>\n      {showCloseButton && (\n        <IconButton onClick={onClose} aria-label=\"Close dialog\" type=\"button\">\n          ✕\n        </IconButton>\n      )}\n    </header>\n  )\n}\n\n/*\n * DialogContent\n */\n\nexport type DialogContentProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogContent = ({ children, className }: DialogContentProps) => {\n  return <div className={clsx(s.content, className)}>{children}</div>\n}\n\n/*\n * DialogFooter\n */\n\nexport type DialogFooterProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogFooter = ({ children, className }: DialogFooterProps) => {\n  return <footer className={clsx(s.footer, className)}>{children}</footer>\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Dialog/index.ts",
    "content": "export * from './Dialog'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/DropdownMenu/DropdownMenu.module.css",
    "content": ".container {\n  position: relative;\n  display: inline-block;\n}\n\n.trigger {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.trigger:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.trigger:enabled:hover,\n.trigger:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  position: fixed;\n  z-index: 50;\n\n  min-width: 160px;\n  padding: 4px;\n  border-radius: 8px;\n\n  background-color: var(--color-bg-primary);\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n}\n\n.content.align-start {\n  transform-origin: top left;\n}\n\n.content.align-center {\n  transform-origin: top center;\n  transform: translateX(-50%);\n}\n\n.content.align-end {\n  transform-origin: top right;\n  transform: translateX(-100%);\n}\n\n.content.side-top {\n  transform-origin: bottom;\n}\n\n.content.side-top.align-center {\n  transform: translateX(-50%) translateY(-100%);\n}\n\n.content.side-top.align-end {\n  transform: translateX(-100%) translateY(-100%);\n}\n\n.content.side-top.align-start {\n  transform: translateY(-100%);\n}\n\n.item {\n  cursor: pointer;\n\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: none;\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-align: left;\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.item:focus-visible {\n  background-color: var(--color-accent);\n  outline: none;\n}\n\n.item:hover:not(:disabled) {\n  background-color: var(--color-accent);\n}\n\n.itemDisabled {\n  cursor: not-allowed;\n  color: var(--color-text-secondary);\n  opacity: 0.5;\n}\n\n.itemDisabled:hover {\n  background: transparent;\n}\n\n.separator {\n  height: 1px;\n  margin: 4px 0;\n  background-color: var(--color-border-base);\n}\n\n/* Animations */\n@keyframes dropdown-menu-show {\n  from {\n    transform: scale(0.95);\n    opacity: 0;\n  }\n\n  to {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { CreateIcon, MoreIcon, PlusIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from './DropdownMenu'\n\nconst meta: Meta<typeof DropdownMenu> = {\n  title: 'Components/DropdownMenu',\n  component: DropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const BasicDropdownMenu: Story = {\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n          Show text song\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithIcons: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist')}>\n          <PlusIcon />\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithDisabledItem: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem disabled>Add to playlist (disabled)</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const CustomTrigger: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <IconButton aria-label=\"More options\">\n          <MoreIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Action 1')}>Action 1</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 2')}>Action 2</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 3')}>Action 3</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const DifferentAlignments: Story = {\n  args: {},\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '100px',\n        padding: '100px',\n        alignItems: 'center',\n        backgroundColor: 'var(--color-bg-secondary)',\n      }}>\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align Start\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align Center\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"center\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align End (default)\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </div>\n  ),\n}\n\nexport const WithLinks: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          as=\"a\"\n          href=\"https://example.com\"\n          target=\"_blank\"\n          onClick={() => console.log('Link clicked')}>\n          <PlusIcon />\n          Visit Website\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const Interactive: Story = {\n  args: {},\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '20px',\n        alignItems: 'center',\n        padding: '40px',\n      }}>\n      <Typography variant=\"h3\">Click the menu buttons to test functionality</Typography>\n\n      <div style={{ display: 'flex', gap: '20px' }}>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent>\n            <DropdownMenuItem onClick={() => console.log('Edit clicked')}>\n              <CreateIcon />\n              Edit track\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Add to playlist clicked')}>\n              <PlusIcon />\n              Add to playlist\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem\n              as=\"a\"\n              href=\"https://example.com\"\n              target=\"_blank\"\n              onClick={() => console.log('External link clicked')}>\n              Show lyrics online\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Download clicked')}>\n              Download\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem disabled>Share (coming soon)</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <IconButton aria-label=\"Playlist options\">\n              <MoreIcon />\n            </IconButton>\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem onClick={() => console.log('Edit playlist')}>\n              Edit playlist\n            </DropdownMenuItem>\n            <DropdownMenuItem\n              as=\"a\"\n              href=\"/share/playlist\"\n              onClick={() => console.log('Share playlist')}>\n              Share playlist\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Delete playlist')}>\n              Delete playlist\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <Typography variant=\"caption\">Open browser console to see click events</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/DropdownMenu/DropdownMenu.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  createContext,\n  type ElementType,\n  type ReactNode,\n  use,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\n\nimport s from './DropdownMenu.module.css'\n\ntype DropdownMenuContextType = {\n  isOpen: boolean\n  onClose: () => void\n  onToggle: () => void\n  triggerRef: React.RefObject<HTMLElement | null>\n}\n\nconst DropdownMenuContext = createContext<DropdownMenuContextType | null>(null)\n\nconst useDropdownMenuContext = () => {\n  const context = use(DropdownMenuContext)\n  if (!context) {\n    throw new Error('DropdownMenu compound components must be used within DropdownMenu component')\n  }\n  return context\n}\n\n/*\n * DropdownMenu\n */\n\nexport type DropdownMenuProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DropdownMenu = ({ children, className }: DropdownMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false)\n  const triggerRef = useRef<HTMLElement>(null)\n\n  const onClose = () => setIsOpen(false)\n  const onToggle = () => setIsOpen(!isOpen)\n\n  useBlockScroll({ isOpen, triggerRef })\n\n  // Close on escape key\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n    return () => document.removeEventListener('keydown', handleKeyDown)\n  }, [isOpen])\n\n  // Close on click outside\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as Element\n      if (\n        triggerRef.current &&\n        !triggerRef.current.contains(target) &&\n        !target.closest('[data-dropdown-content]')\n      ) {\n        onClose()\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen])\n\n  const contextValue = {\n    isOpen,\n    onClose,\n    onToggle,\n    triggerRef,\n  }\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <DropdownMenuContext value={contextValue}>{children}</DropdownMenuContext>\n    </div>\n  )\n}\n\n/*\n * DropdownMenuTrigger\n */\n\nexport type DropdownMenuTriggerProps = {\n  children: ReactNode\n  className?: string\n  asChild?: boolean\n}\n\nexport const DropdownMenuTrigger = ({\n  children,\n  className,\n  asChild = false,\n}: DropdownMenuTriggerProps) => {\n  const { onToggle, triggerRef } = useDropdownMenuContext()\n\n  if (asChild) {\n    return (\n      <div\n        ref={triggerRef as React.RefObject<HTMLDivElement>}\n        onClick={onToggle}\n        className={className}>\n        {children}\n      </div>\n    )\n  }\n\n  return (\n    <button\n      ref={triggerRef as React.RefObject<HTMLButtonElement>}\n      type=\"button\"\n      onClick={onToggle}\n      className={clsx(s.trigger, className)}>\n      {children}\n    </button>\n  )\n}\n\n/*\n * DropdownMenuContent\n */\n\nexport type DropdownMenuContentProps = {\n  children: ReactNode\n  className?: string\n  align?: 'start' | 'center' | 'end'\n  side?: 'top' | 'bottom' | 'left' | 'right'\n}\n\nexport const DropdownMenuContent = ({\n  children,\n  className,\n  align = 'end',\n  side = 'bottom',\n}: DropdownMenuContentProps) => {\n  const { isOpen, triggerRef } = useDropdownMenuContext()\n  const [position, setPosition] = useState({ top: 0, left: 0 })\n\n  // it's needed to prevent flickering\n  const [isPositioned, setIsPositioned] = useState(false)\n\n  useEffect(() => {\n    if (!isOpen || !triggerRef.current) {\n      setIsPositioned(false)\n      return\n    }\n\n    const triggerRect = triggerRef.current.getBoundingClientRect()\n    const scrollTop = window.pageYOffset || document.documentElement.scrollTop\n    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft\n\n    let top = 0\n    let left = 0\n\n    // Calculate position based on side\n    switch (side) {\n      case 'bottom':\n        top = triggerRect.bottom + scrollTop + 4\n        break\n      case 'top':\n        top = triggerRect.top + scrollTop - 4\n        break\n      case 'right':\n        left = triggerRect.right + scrollLeft + 4\n        top = triggerRect.top + scrollTop\n        break\n      case 'left':\n        left = triggerRect.left + scrollLeft - 4\n        top = triggerRect.top + scrollTop\n        break\n    }\n\n    // Calculate position based on align\n    if (side === 'bottom' || side === 'top') {\n      switch (align) {\n        case 'start':\n          left = triggerRect.left + scrollLeft\n          break\n        case 'center':\n          left = triggerRect.left + scrollLeft + triggerRect.width / 2\n          break\n        case 'end':\n          left = triggerRect.right + scrollLeft\n          break\n      }\n    }\n\n    setPosition({ top, left })\n    setIsPositioned(true)\n  }, [isOpen, align, side])\n\n  if (!isOpen || !isPositioned) return null\n\n  const content = (\n    <div\n      className={clsx(s.content, s[`align-${align}`], s[`side-${side}`], className)}\n      style={{ top: position.top, left: position.left }}\n      data-dropdown-content\n      role=\"menu\">\n      {children}\n    </div>\n  )\n\n  return createPortal(content, document.body)\n}\n\n/*\n * DropdownMenuItem\n */\n\nexport type DropdownMenuItemProps<T extends ElementType = 'button'> = {\n  as?: T\n  children: ReactNode\n  onClick?: () => void\n  className?: string\n  disabled?: boolean\n} & ComponentProps<T>\n\nexport const DropdownMenuItem = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  onClick,\n  className,\n  disabled = false,\n  ...props\n}: DropdownMenuItemProps<T>) => {\n  const { onClose } = useDropdownMenuContext()\n\n  const handleClick = () => {\n    if (disabled) return\n    onClick?.()\n    onClose()\n  }\n\n  const isButton = Component === 'button'\n\n  return (\n    <Component\n      {...(isButton && { type: 'button' })}\n      className={clsx(s.item, disabled && s.itemDisabled, className)}\n      onClick={handleClick}\n      {...(isButton && { disabled })}\n      role=\"menuitem\"\n      {...props}>\n      {children}\n    </Component>\n  )\n}\n\n/*\n * DropdownMenuSeparator\n */\n\nexport type DropdownMenuSeparatorProps = {\n  className?: string\n}\n\nexport const DropdownMenuSeparator = ({ className }: DropdownMenuSeparatorProps) => {\n  return <div className={clsx(s.separator, className)} role=\"separator\" />\n}\n\n/**\n * Block scroll when menu is open.\n */\nconst useBlockScroll = ({\n  isOpen,\n  triggerRef,\n}: {\n  isOpen: boolean\n  triggerRef: React.RefObject<HTMLElement | null>\n}) => {\n  // Block scroll when menu is open\n  useEffect(() => {\n    if (!isOpen || !triggerRef.current) return\n\n    const originalScrollElements: Array<{ element: Element; overflow: string }> = []\n\n    // Find all scrollable parent elements\n    const findScrollableParents = (element: Element) => {\n      const scrollableElements: Element[] = []\n      let parent = element.parentElement\n\n      while (parent && parent !== document.body) {\n        const style = window.getComputedStyle(parent)\n        const hasVerticalScroll =\n          style.overflowY === 'auto' ||\n          style.overflowY === 'scroll' ||\n          style.overflow === 'auto' ||\n          style.overflow === 'scroll'\n\n        if (hasVerticalScroll && parent.scrollHeight > parent.clientHeight) {\n          scrollableElements.push(parent)\n        }\n        parent = parent.parentElement\n      }\n\n      return scrollableElements\n    }\n\n    // Block scroll on body\n    const bodyOverflow = document.body.style.overflow\n    document.body.style.overflow = 'hidden'\n    originalScrollElements.push({ element: document.body, overflow: bodyOverflow })\n\n    // Block scroll on scrollable parents\n    const scrollableParents = findScrollableParents(triggerRef.current)\n    scrollableParents.forEach((element) => {\n      const originalOverflow = (element as HTMLElement).style.overflow\n      ;(element as HTMLElement).style.overflow = 'hidden'\n      originalScrollElements.push({ element, overflow: originalOverflow })\n    })\n\n    return () => {\n      // Restore original overflow values\n      originalScrollElements.forEach(({ element, overflow }) => {\n        ;(element as HTMLElement).style.overflow = overflow\n      })\n    }\n  }, [isOpen])\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/DropdownMenu/index.ts",
    "content": "export * from './DropdownMenu'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Hashtag/Tag.module.css",
    "content": ".hashtag {\n  cursor: pointer;\n\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n\n  min-width: 73px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 45px;\n\n  font-size: var(--font-size-xxxs);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  text-decoration: none;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.hashtag:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.hashtag:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.active {\n  color: var(--color-bg-primary);\n  background-color: var(--color-text-primary);\n}\n\n.active:hover:not(:disabled) {\n  color: var(--color-bg-primary);\n  opacity: 0.9;\n  background-color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Hashtag/Tag.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Tag } from './Tag.tsx'\n\nconst meta = {\n  title: 'Components/Hashtag',\n  component: Tag,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    tag: 'Playlists',\n  },\n} satisfies Meta<typeof Tag>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n\nexport const Active: Story = {\n  args: {\n    active: true,\n  },\n}\n\nexport const AsLink: Story = {\n  args: {\n    as: 'a',\n    href: 'https://www.google.com',\n    target: '_blank',\n  },\n}\n\nexport const AllHashtags: Story = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '16px' }}>\n      <Tag tag=\"Playlists\" />\n      <Tag active tag=\"Artists\" />\n      <Tag tag=\"Albums\" />\n      <Tag as=\"a\" href=\"#\" tag=\"Podcasts & shows\">\n        Podcasts & shows\n      </Tag>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Hashtag/Tag.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Tag.module.css'\n\nexport type HashtagProps<T extends ElementType = 'button'> = {\n  as?: T\n  active?: boolean\n  tag: string\n  className?: string\n} & ComponentProps<T>\n\nexport const Tag = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  active = false,\n  tag,\n  className,\n  ...props\n}: HashtagProps<T>) => {\n  const classNames = clsx(s.hashtag, active && s.active, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      #{tag}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Hashtag/index.ts",
    "content": "export * from './Tag.tsx'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/IconButton/IconButton.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.button:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.button:enabled:hover,\n.button:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/IconButton/IconButton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport {\n  DownloadIcon,\n  HomeIcon,\n  LikeIcon,\n  MoreIcon,\n  PlayIcon,\n  PlusIcon,\n  SearchIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from './IconButton'\n\nconst meta = {\n  title: 'Components/IconButton',\n  component: IconButton,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof IconButton>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    children: <PlayIcon />,\n    'aria-label': 'Play',\n  },\n}\n\nexport const AllIcons = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '16px',\n        flexWrap: 'wrap',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: '20px',\n      }}>\n      <IconButton aria-label=\"Home\">\n        <HomeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Search\">\n        <SearchIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Play\">\n        <PlayIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Like\">\n        <LikeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Add\">\n        <PlusIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"More options\">\n        <MoreIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Download\">\n        <DownloadIcon />\n      </IconButton>\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    children: <PlayIcon />,\n    disabled: true,\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/IconButton/IconButton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './IconButton.module.css'\n\ntype IconButtonProps = {\n  children: React.ReactNode\n} & ComponentProps<'button'>\n\nexport const IconButton = ({ children, className, ...props }: IconButtonProps) => {\n  return (\n    <button type=\"button\" className={clsx(s.button, className)} {...props}>\n      {children}\n    </button>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/IconButton/index.ts",
    "content": "export * from './IconButton'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/ImageUploader/ImageUploader.module.css",
    "content": ".container {\n  width: 100%;\n}\n\n.dropZone {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 100%;\n  min-height: 280px;\n  border: 2px dashed var(--color-border-input-primary);\n  border-radius: 8px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover,\n.dropZone:focus-within {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.dragOver {\n  border-color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.hasPreview {\n  border-color: var(--color-border-input-active);\n  border-style: solid;\n}\n\n.dropZone.error {\n  border-color: var(--color-text-error);\n}\n\n.hiddenInput {\n  position: absolute;\n\n  overflow: hidden;\n\n  width: 1px;\n  height: 1px;\n\n  opacity: 0;\n  clip: rect(0, 0, 0, 0);\n}\n\n.uploadContent {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  align-items: center;\n\n  padding: 32px 16px;\n}\n\n.uploadIcon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n\n  color: var(--color-text-secondary);\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover .uploadIcon,\n.dropZone:focus-within .uploadIcon {\n  color: var(--color-accent);\n  background-color: var(--color-bg-card);\n}\n\n.uploadText {\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  transition: color 200ms ease;\n}\n\n.dropZone:hover .uploadText {\n  color: var(--color-text-primary);\n}\n\n.previewContainer {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.previewImage {\n  width: 100%;\n  height: 100%;\n  min-height: 200px;\n  border-radius: 6px;\n\n  object-fit: cover;\n}\n\n.removeButton {\n  position: absolute;\n  top: 8px;\n  right: 8px;\n}\n\n.removeButton:hover {\n  opacity: 1;\n  background-color: var(--color-text-error);\n}\n\n.removeButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.errorMessage {\n  margin-top: 8px;\n}\n\n/* States for different sizes */\n.dropZone.small {\n  min-height: 120px;\n}\n\n.dropZone.large {\n  min-height: 300px;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/ImageUploader/ImageUploader.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ImageUploader } from './ImageUploader'\n\nconst meta = {\n  title: 'Components/ImageUploader',\n  component: ImageUploader,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    onImageSelect: () => {},\n  },\n} satisfies Meta<typeof ImageUploader>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    placeholder: 'Upload Cover Image',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const CustomPlaceholder: Story = {\n  args: {\n    placeholder: 'Choose your avatar',\n  },\n  render: (args) => (\n    <div style={{ width: '200px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const WithCustomLimits: Story = {\n  args: {\n    placeholder: 'Upload image (max 2MB)',\n    maxSizeInMB: 2,\n    acceptedFormats: ['image/jpeg', 'image/png'],\n  },\n  render: (args) => (\n    <div style={{ width: '400px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const AllowAllImages: Story = {\n  args: {\n    placeholder: 'Upload any image format',\n    acceptedFormats: ['image/*'],\n    maxSizeInMB: 10,\n  },\n  render: (args) => (\n    <div style={{ width: '350px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const Interactive: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '400px',\n      }}>\n      <div>\n        <h3 style={{ color: 'var(--color-text-primary)', marginBottom: '12px' }}>Profile Avatar</h3>\n        <ImageUploader\n          placeholder=\"Upload avatar\"\n          onImageSelect={(file) => console.log('Avatar selected:', file.name)}\n          maxSizeInMB={1}\n        />\n      </div>\n\n      <div>\n        <h3 style={{ color: 'var(--color-text-primary)', marginBottom: '12px' }}>Playlist Cover</h3>\n        <ImageUploader\n          placeholder=\"Upload Cover Image\"\n          onImageSelect={(file) => console.log('Cover selected:', file.name)}\n          maxSizeInMB={5}\n        />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/ImageUploader/ImageUploader.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ChangeEvent, type DragEvent, useRef, useState } from 'react'\n\nimport { ImageUploadIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './ImageUploader.module.css'\n\nexport type ImageUploaderProps = {\n  onImageSelect: (file: File) => void\n  className?: string\n  acceptedFormats?: string[]\n  maxSizeInMB?: number\n  placeholder?: string\n}\n\nexport const ImageUploader = ({\n  className,\n  onImageSelect,\n  acceptedFormats = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'],\n  maxSizeInMB = 5,\n  placeholder = 'Upload Cover Image',\n}: ImageUploaderProps) => {\n  const [isDragOver, setIsDragOver] = useState(false)\n  const [preview, setPreview] = useState<string | null>(null)\n  const [error, setError] = useState<string | null>(null)\n  const fileInputRef = useRef<HTMLInputElement>(null)\n\n  const validateFile = (file: File): string | null => {\n    if (!acceptedFormats.includes(file.type)) {\n      return `Only ${acceptedFormats.join(', ')} files are allowed`\n    }\n\n    const maxSizeInBytes = maxSizeInMB * 1024 * 1024\n    if (file.size > maxSizeInBytes) {\n      return `File size must be less than ${maxSizeInMB}MB`\n    }\n\n    return null\n  }\n\n  const handleFileSelect = (file: File) => {\n    const validationError = validateFile(file)\n\n    if (validationError) {\n      setError(validationError)\n      setPreview(null)\n      return\n    }\n\n    setError(null)\n\n    // Create preview\n    const reader = new FileReader()\n    reader.onload = (e) => {\n      setPreview(e.target?.result as string)\n    }\n    reader.readAsDataURL(file)\n\n    onImageSelect(file)\n  }\n\n  const handleFileInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0]\n    if (file) {\n      handleFileSelect(file)\n    }\n  }\n\n  const handleDragOver = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(true)\n  }\n\n  const handleDragLeave = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n  }\n\n  const handleDrop = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n\n    const files = Array.from(e.dataTransfer.files)\n    const imageFile = files.find((file) => file.type.startsWith('image/'))\n\n    if (imageFile) {\n      handleFileSelect(imageFile)\n    }\n  }\n\n  const handleRemoveImage = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    setPreview(null)\n    setError(null)\n    // Clear input value to allow selecting the same file again\n    if (fileInputRef.current) {\n      fileInputRef.current.value = ''\n    }\n  }\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <label\n        className={clsx(\n          s.dropZone,\n          isDragOver && s.dragOver,\n          preview && s.hasPreview,\n          error && s.error\n        )}\n        onDragOver={handleDragOver}\n        onDragLeave={handleDragLeave}\n        onDrop={handleDrop}>\n        <input\n          ref={fileInputRef}\n          type=\"file\"\n          accept={acceptedFormats.join(',')}\n          onChange={handleFileInputChange}\n          className={s.hiddenInput}\n          tabIndex={0}\n        />\n\n        {preview ? (\n          <div className={s.previewContainer}>\n            <img src={preview} alt=\"Preview\" className={s.previewImage} />\n            <IconButton\n              className={s.removeButton}\n              onClick={handleRemoveImage}\n              aria-label=\"Remove image\"\n              type=\"button\">\n              ✕\n            </IconButton>\n          </div>\n        ) : (\n          <div className={s.uploadContent}>\n            <div className={s.uploadIcon}>\n              <ImageUploadIcon width={24} height={24} />\n            </div>\n            <Typography variant=\"body2\" className={s.uploadText}>\n              {placeholder}\n            </Typography>\n          </div>\n        )}\n      </label>\n\n      {error && (\n        <Typography variant=\"error\" className={s.errorMessage}>\n          {error}\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/ImageUploader/index.ts",
    "content": "export * from './ImageUploader'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Pagination/Pagination.module.css",
    "content": ".pagination {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n}\n\n.navButton {\n  width: 40px;\n  height: 40px;\n  border-radius: 4px;\n\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.navButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n  background-color: var(--color-bg-secondary);\n}\n\n.navButton:enabled:hover,\n.navButton:enabled:focus {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageNumbers {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n}\n\n.pageButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n  border: none;\n  border-radius: 8px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.pageButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.pageButton:hover:not(.active) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageButton.active {\n  background-color: var(--color-accent);\n}\n\n.pageButton.active:hover {\n  opacity: 0.9;\n}\n\n.ellipsis {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* Responsive adjustments */\n@media (width <= 480px) {\n  .pagination {\n    gap: 2px;\n  }\n\n  .navButton,\n  .pageButton,\n  .ellipsis {\n    width: 36px;\n    height: 36px;\n  }\n\n  .pageButton,\n  .ellipsis {\n    font-size: var(--font-size-s);\n  }\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Pagination/Pagination.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Pagination } from './Pagination'\n\nconst meta = {\n  title: 'Components/Pagination',\n  component: Pagination,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Pagination>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    page: 1,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const MiddlePage: Story = {\n  args: {\n    page: 5,\n    pagesCount: 10,\n    onPageChange: () => {},\n  },\n}\n\nexport const LastPage: Story = {\n  args: {\n    page: 3,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const ManyPages: Story = {\n  args: {\n    page: 8,\n    pagesCount: 20,\n    onPageChange: () => {},\n  },\n}\n\nexport const SinglePage: Story = {\n  args: {\n    page: 1,\n    pagesCount: 1,\n    onPageChange: () => {},\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [currentPage, setCurrentPage] = useState(1)\n    const totalCount = 95\n    const pageSize = 10\n    const pagesCount = Math.ceil(totalCount / pageSize)\n\n    const handlePageChange = (page: number) => {\n      setCurrentPage(page)\n    }\n\n    return (\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n          alignItems: 'center',\n          width: '500px',\n        }}>\n        <Card style={{ padding: '20px', textAlign: 'center' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Interactive Pagination\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Current page: <strong>{currentPage}</strong>\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Total items: <strong>{totalCount}</strong>\n          </Typography>\n          <Typography variant=\"body2\">\n            Items per page: <strong>{pageSize}</strong>\n          </Typography>\n        </Card>\n\n        <Pagination page={currentPage} pagesCount={pagesCount} onPageChange={handlePageChange} />\n\n        <Typography variant=\"caption\" style={{ textAlign: 'center' }}>\n          Click on page numbers or arrows to navigate\n        </Typography>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '32px',\n        alignItems: 'center',\n        width: '600px',\n      }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          First Page (3 pages total)\n        </Typography>\n        <Pagination page={1} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Middle Page (10 pages total)\n        </Typography>\n        <Pagination page={5} pagesCount={10} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Last Page (3 pages total)\n        </Typography>\n        <Pagination page={3} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Many Pages (20 pages total)\n        </Typography>\n        <Pagination page={12} pagesCount={20} onPageChange={() => {}} />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Pagination/Pagination.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport { KeyboardArrowLeftIcon, KeyboardArrowRightIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './Pagination.module.css'\n\nexport type PaginationProps = {\n  page: number\n  pagesCount: number\n  onPageChange: (page: number) => void\n  className?: string\n} & Omit<ComponentProps<'div'>, 'children'>\n\nconst MAX_VISIBLE_PAGES = 5\n\nexport const Pagination = ({\n  page,\n\n  pagesCount,\n  onPageChange,\n  className,\n  ...props\n}: PaginationProps) => {\n  // Helper function to generate page numbers array\n  const generatePageNumbers = () => {\n    const pages: (number | 'ellipsis')[] = []\n\n    if (pagesCount <= MAX_VISIBLE_PAGES) {\n      // Show all pages if total is small\n      for (let i = 1; i <= pagesCount; i++) {\n        pages.push(i)\n      }\n    } else {\n      // Always show first page\n      pages.push(1)\n\n      if (page > 3) {\n        pages.push('ellipsis')\n      }\n\n      // Show pages around current page\n      const start = Math.max(2, page - 1)\n      const end = Math.min(pagesCount - 1, page + 1)\n\n      for (let i = start; i <= end; i++) {\n        if (i !== 1 && i !== pagesCount) {\n          pages.push(i)\n        }\n      }\n\n      if (page < pagesCount - 2) {\n        pages.push('ellipsis')\n      }\n\n      // Always show last page if it's not already included\n      if (pagesCount > 1) {\n        pages.push(pagesCount)\n      }\n    }\n\n    return pages\n  }\n\n  const handlePrevious = () => {\n    if (page > 1) {\n      onPageChange(page - 1)\n    }\n  }\n\n  const handleNext = () => {\n    if (page < pagesCount) {\n      onPageChange(page + 1)\n    }\n  }\n\n  const handlePageClick = (pageNumber: number) => {\n    onPageChange(pageNumber)\n  }\n\n  if (pagesCount <= 1) {\n    return null\n  }\n\n  const pageNumbers = generatePageNumbers()\n\n  return (\n    <div\n      className={clsx(s.pagination, className)}\n      role=\"navigation\"\n      aria-label=\"Pagination\"\n      {...props}>\n      {/* Previous button */}\n      <IconButton\n        onClick={handlePrevious}\n        disabled={page === 1}\n        aria-label=\"Go to previous page\"\n        className={s.navButton}>\n        <KeyboardArrowLeftIcon />\n      </IconButton>\n\n      {/* Page numbers */}\n      <div className={s.pageNumbers}>\n        {pageNumbers.map((pageNumber, index) => {\n          if (pageNumber === 'ellipsis') {\n            return (\n              <span key={`ellipsis-${index}`} className={s.ellipsis} aria-hidden=\"true\">\n                ...\n              </span>\n            )\n          }\n\n          const isActive = pageNumber === page\n\n          return (\n            <button\n              key={pageNumber}\n              onClick={() => handlePageClick(pageNumber)}\n              className={clsx(s.pageButton, isActive && s.active)}\n              aria-label={`Go to page ${pageNumber}`}\n              aria-current={isActive ? 'page' : undefined}\n              type=\"button\">\n              {pageNumber}\n            </button>\n          )\n        })}\n      </div>\n\n      {/* Next button */}\n      <IconButton\n        onClick={handleNext}\n        disabled={page === pagesCount}\n        aria-label=\"Go to next page\"\n        className={s.navButton}>\n        <KeyboardArrowRightIcon />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Pagination/index.ts",
    "content": "export * from './Pagination'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Progress/Progress.module.css",
    "content": ".progress {\n  overflow: hidden;\n\n  width: 100%;\n  height: 4px;\n  border-radius: 4px;\n\n  background-color: var(--color-border-base);\n}\n\n.progressBar {\n  height: 100%;\n  border-radius: 4px;\n  background-color: var(--color-accent);\n  transition: width 300ms ease;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Progress/Progress.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Progress } from './Progress'\n\nconst meta = {\n  title: 'Components/Progress',\n  component: Progress,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Progress>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    value: 75,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const CustomMax: Story = {\n  args: {\n    value: 15,\n    max: 20,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Empty: Story = {\n  args: {\n    value: 0,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Full: Story = {\n  args: {\n    value: 100,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Empty (0%)\n        </Typography>\n        <Progress value={0} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Low (25%)\n        </Typography>\n        <Progress value={25} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Medium (50%)\n        </Typography>\n        <Progress value={50} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          High (85%)\n        </Typography>\n        <Progress value={85} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Complete (100%)\n        </Typography>\n        <Progress value={100} />\n      </div>\n    </div>\n  ),\n}\n\nexport const CustomSizes = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Small (height: 4px)\n        </Typography>\n        <Progress value={70} style={{ height: '4px' }} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Default (height: 8px)\n        </Typography>\n        <Progress value={70} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Large (height: 12px)\n        </Typography>\n        <Progress value={70} style={{ height: '12px' }} />\n      </div>\n    </div>\n  ),\n}\n\nexport const Interactive = {\n  render: () => {\n    const [progress, setProgress] = useState(0)\n\n    const handleIncrease = () => {\n      setProgress((prev) => Math.min(prev + 10, 100))\n    }\n\n    const handleDecrease = () => {\n      setProgress((prev) => Math.max(prev - 10, 0))\n    }\n\n    const handleReset = () => {\n      setProgress(0)\n    }\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Card style={{ padding: '24px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Interactive Progress\n          </Typography>\n\n          <div style={{ marginBottom: '16px' }}>\n            <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n              Current progress: {progress}%\n            </Typography>\n            <Progress value={progress} />\n          </div>\n\n          <div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>\n            <Button variant=\"secondary\" onClick={handleDecrease} disabled={progress === 0}>\n              -10%\n            </Button>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset\n            </Button>\n            <Button variant=\"primary\" onClick={handleIncrease} disabled={progress === 100}>\n              +10%\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const FileUploadExample = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Card style={{ padding: '24px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          File Upload Progress\n        </Typography>\n\n        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">image.jpg</Typography>\n              <Typography variant=\"body2\">75%</Typography>\n            </div>\n            <Progress value={75} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">document.pdf</Typography>\n              <Typography variant=\"body2\">100%</Typography>\n            </div>\n            <Progress value={100} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">video.mp4</Typography>\n              <Typography variant=\"body2\">32%</Typography>\n            </div>\n            <Progress value={32} />\n          </div>\n        </div>\n      </Card>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Progress/Progress.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './Progress.module.css'\n\nexport type ProgressProps = {\n  value: number\n  max?: number\n} & ComponentProps<'div'>\n\nexport const Progress = ({ value, max = 100, className, ...props }: ProgressProps) => {\n  const percentage = Math.min(Math.max((value / max) * 100, 0), 100)\n\n  return (\n    <div\n      className={clsx(s.progress, className)}\n      role=\"progressbar\"\n      aria-valuenow={value}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      {...props}>\n      <div className={s.progressBar} style={{ width: `${percentage}%` }} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Progress/index.ts",
    "content": "export * from './Progress'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/ReactionButtons/ReactionButtons.module.css",
    "content": ".container {\n  display: flex;\n  gap: 8px;\n  align-items: start;\n}\n\n.button {\n  width: 28px;\n  height: 28px;\n  padding: 0;\n  transition: color 200ms ease;\n}\n\n.button.large {\n  width: 40px;\n  height: 40px;\n}\n\n.button.liked {\n  color: var(--color-accent);\n}\n\n.button.disliked {\n  color: var(--color-accent);\n}\n\n.button:enabled:hover:is(.liked, .disliked),\n.button:enabled:focus:is(.liked, .disliked) {\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.likesCountBox {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.likesCount {\n  font-size: 10px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/ReactionButtons/ReactionButtons.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { type CurrentUserReaction, ReactionButtons } from './ReactionButtons'\n\nconst meta = {\n  title: 'Components/ReactionButtons',\n  component: ReactionButtons,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof ReactionButtons>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    reaction: 0,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n  },\n}\n\nexport const WithLikesCount: Story = {\n  args: {\n    reaction: 0,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n    likesCount: 10,\n  },\n}\n\nexport const LikedState: Story = {\n  args: {\n    reaction: 1,\n    onLike: () => console.log('Unlike'),\n    onDislike: () => console.log('Disliked!'),\n  },\n}\n\nexport const DislikedState: Story = {\n  args: {\n    reaction: -1,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Remove dislike'),\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [reaction, setReaction] = useState<CurrentUserReaction>(0)\n\n    const handleLike = () => {\n      setReaction(reaction === 1 ? 0 : 1)\n    }\n\n    const handleDislike = () => {\n      setReaction(reaction === -1 ? 0 : -1)\n    }\n\n    return (\n      <Card style={{ padding: '24px', maxWidth: '300px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          Interactive Reaction Buttons\n        </Typography>\n\n        <Typography variant=\"body2\" style={{ marginBottom: '16px' }}>\n          Try clicking the buttons below:\n        </Typography>\n\n        <div style={{ display: 'flex', justifyContent: 'center' }}>\n          <ReactionButtons reaction={reaction} onLike={handleLike} onDislike={handleDislike} />\n        </div>\n\n        <Typography\n          variant=\"caption\"\n          style={{ marginTop: '16px', textAlign: 'center', display: 'block' }}>\n          Status: {reaction === 1 ? '👍 Liked' : reaction === -1 ? '👎 Disliked' : '😐 Neutral'}\n        </Typography>\n      </Card>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Default\n        </Typography>\n        <ReactionButtons reaction={0} onLike={() => {}} onDislike={() => {}} />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Liked\n        </Typography>\n        <ReactionButtons reaction={1} onLike={() => {}} onDislike={() => {}} />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Disliked\n        </Typography>\n        <ReactionButtons reaction={-1} onLike={() => {}} onDislike={() => {}} />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          With likes count\n        </Typography>\n        <ReactionButtons reaction={0} onLike={() => {}} onDislike={() => {}} likesCount={10} />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/ReactionButtons/ReactionButtons.tsx",
    "content": "import { clsx } from 'clsx'\n\nimport type { ReactionValue } from '@/shared/api/schema.ts'\nimport { DislikeIcon, LikeIcon, LikeIconFill } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './ReactionButtons.module.css'\n\n// duplication of the CurrentUserReaction type to decouple the shared layer from the features layer\nexport type CurrentUserReaction = ReactionValue\n\nexport type ReactionButtonsProps = {\n  reaction?: CurrentUserReaction\n  onLike: () => void\n  onDislike: () => void\n  likesCount?: number\n  className?: string\n  size?: keyof typeof SIZE_MAP\n}\n\nconst SIZE_MAP = {\n  small: 28,\n  large: 40,\n}\n\nexport const ReactionButtons = ({\n  reaction = 0,\n  onLike,\n  onDislike,\n  likesCount,\n  className,\n  size = 'small',\n}: ReactionButtonsProps) => {\n  const isLiked = reaction === 1\n  const isDisliked = reaction === -1\n\n  const iconSize = SIZE_MAP[size]\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <div className={s.likesCountBox}>\n        <IconButton\n          onClick={(e) => {\n            e.preventDefault()\n            onLike()\n          }}\n          className={clsx(s.button, isLiked && s.liked, size === 'large' && s.large)}\n          aria-label={isLiked ? 'Remove like' : 'Like'}\n          type=\"button\">\n          {isLiked ? (\n            <LikeIconFill width={iconSize} height={iconSize} />\n          ) : (\n            <LikeIcon width={iconSize} height={iconSize} />\n          )}\n        </IconButton>\n        <span className={s.likesCount}>{likesCount}</span>\n      </div>\n\n      <IconButton\n        onClick={(e) => {\n          e.preventDefault()\n          onDislike()\n        }}\n        className={clsx(s.button, isDisliked && s.disliked, size === 'large' && s.large)}\n        aria-label={isDisliked ? 'Remove dislike' : 'Dislike'}\n        type=\"button\">\n        <DislikeIcon width={iconSize} height={iconSize} />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/ReactionButtons/index.ts",
    "content": "export * from './ReactionButtons'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/SearchField/SearchField.module.css",
    "content": ".inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.searchIcon {\n  pointer-events: none;\n\n  position: absolute;\n  z-index: 1;\n  left: 12px;\n\n  color: var(--color-text-secondary);\n\n  transition: color 200ms ease;\n}\n\n.input {\n  width: 100%;\n  height: 52px;\n  padding: 15px 16px 15px 62px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 26px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary-reverse);\n\n  background-color: var(--color-bg-primary-reverse);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input::placeholder {\n  font-size: var(--font-size-m);\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/SearchField/SearchField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchField } from './SearchField'\n\nconst meta = {\n  title: 'Components/SearchField',\n  component: SearchField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof SearchField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    placeholder: 'Search for playlists...',\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/SearchField/SearchField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport s from './SearchField.module.css'\n\nexport type SearchFieldProps = {\n  label?: ReactNode\n  placeholder?: string\n} & ComponentProps<'input'>\n\nexport const SearchField = ({\n  className,\n  placeholder = 'Search...',\n  ...props\n}: SearchFieldProps) => {\n  return (\n    <div className={clsx(s.inputWrapper, className)}>\n      <SearchIcon className={s.searchIcon} />\n      <input className={clsx(s.input)} type=\"text\" placeholder={placeholder} {...props} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/SearchField/index.ts",
    "content": "export * from './SearchField'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Select/Select.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.selectWrapper {\n  position: relative;\n  width: 100%;\n}\n\n.select {\n  width: 100%;\n  height: 40px;\n  padding: 8px 36px 8px 12px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-decoration: underline;\n  text-underline-offset: 3px;\n\n  appearance: none;\n  background-color: transparent;\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color;\n}\n\n.select:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.select:focus-visible {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select.error {\n  border-color: var(--color-text-error);\n}\n\n/* Style dropdown options */\n.select option {\n  padding: 8px 12px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: background-color 200ms ease;\n}\n\n.select option:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:checked {\n  font-weight: 600;\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:disabled {\n  color: var(--color-disabled);\n}\n\n/* Custom dropdown icon */\n.icon {\n  pointer-events: none;\n\n  position: absolute;\n  top: 50%;\n  right: 12px;\n  transform: translateY(-50%);\n\n  width: 20px;\n  height: 20px;\n\n  color: var(--color-text-secondary);\n\n  transition:\n    color 200ms ease,\n    transform 200ms ease;\n}\n\n/* Rotate icon when dropdown is open */\n.select:open + .icon {\n  transform: translateY(-50%) rotate(180deg);\n}\n\n.label.error {\n  color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Select/Select.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Select } from './Select'\n\nconst meta = {\n  title: 'Components/Select',\n  component: Select,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Select>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst commonOptions = [\n  { value: 'react', label: 'React' },\n  { value: 'vue', label: 'Vue.js' },\n  { value: 'angular', label: 'Angular' },\n  { value: 'svelte', label: 'Svelte' },\n  { value: 'vanilla', label: 'Vanilla JS' },\n]\n\nconst genres = [\n  { value: 'pop', label: 'Pop' },\n  { value: 'rock', label: 'Rock' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hip-hop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n]\n\nexport const AllVariants = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '350px',\n      }}>\n      <Select label=\"Basic Select\" placeholder=\"Choose option\" options={commonOptions} />\n\n      <Select label=\"With Default Value\" options={commonOptions} defaultValue=\"react\" />\n\n      <Select\n        label=\"With Error\"\n        placeholder=\"Choose option\"\n        options={commonOptions}\n        errorMessage=\"This field is required\"\n      />\n\n      <Select label=\"Disabled\" placeholder=\"Cannot select\" options={commonOptions} disabled />\n    </div>\n  ),\n}\n\nexport const Basic: Story = {\n  args: {\n    label: 'Choose framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDefaultValue: Story = {\n  args: {\n    label: 'Preferred framework',\n    options: commonOptions,\n    defaultValue: 'react',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Framework (disabled)',\n    placeholder: 'Cannot select',\n    options: commonOptions,\n    disabled: true,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithError: Story = {\n  args: {\n    label: 'Framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n    errorMessage: 'Please select a framework',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDisabledOptions: Story = {\n  args: {\n    label: 'Music Genre',\n    placeholder: 'Choose your favorite genre',\n    options: [\n      { value: 'pop', label: 'Pop' },\n      { value: 'rock', label: 'Rock' },\n      { value: 'jazz', label: 'Jazz (Coming Soon)', disabled: true },\n      { value: 'classical', label: 'Classical' },\n      { value: 'electronic', label: 'Electronic (Coming Soon)', disabled: true },\n      { value: 'hip-hop', label: 'Hip Hop' },\n    ],\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Controlled = {\n  render: () => {\n    const [value, setValue] = useState('')\n\n    return (\n      <div style={{ width: '400px', display: 'flex', flexDirection: 'column', gap: '16px' }}>\n        <Select\n          label=\"Music Genre\"\n          placeholder=\"Select genre\"\n          options={genres}\n          value={value}\n          onChange={(e) => setValue(e.target.value)}\n        />\n\n        <div\n          style={{\n            padding: '12px',\n            backgroundColor: 'var(--color-bg-card)',\n            borderRadius: '4px',\n            fontSize: 'var(--font-size-s)',\n            color: 'var(--color-text-secondary)',\n          }}>\n          Selected value: <strong>{value || 'None'}</strong>\n        </div>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Select/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          as=\"label\"\n          htmlFor={selectId}\n          className={clsx(s.label, showError && s.error)}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Select/index.ts",
    "content": "export * from './Select'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/SortSelect/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={selectId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Table/Table.module.css",
    "content": ".table {\n  table-layout: fixed;\n  border-collapse: collapse;\n  width: 100%;\n  background: transparent;\n}\n\n.tableHead {\n  border-bottom: 1px solid var(--color-border-base);\n}\n\n.tableHeaderCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-xs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  text-align: left;\n  text-transform: uppercase;\n\n  background: transparent;\n}\n\n.tableHeaderCell:first-child {\n  padding-left: 16px;\n}\n\n.tableHeaderCell:last-child {\n  padding-right: 16px;\n}\n\n.tableBody {\n  background: transparent;\n}\n\n.tableRow {\n  transition: background-color 200ms ease;\n}\n\n.tableBody .tableRow:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tableCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  vertical-align: middle;\n\n  background: transparent;\n}\n\n.tableCell:first-child {\n  padding-left: 16px;\n}\n\n.tableCell:last-child {\n  padding-right: 16px;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Table/Table.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ReactionButtons } from '../ReactionButtons'\nimport { Typography } from '../Typography'\nimport { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './Table'\nimport s from './Table.module.css'\n\nconst meta = {\n  title: 'Components/Table',\n  component: Table,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Table>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst trackData = [\n  {\n    id: 1,\n    title: 'Play It Safe',\n    artist: 'Julia Wolf',\n    image: 'https://picsum.photos/40/40?random=1',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 2,\n    title: 'Ocean Front Apt.',\n    artist: 'ayokay',\n    image: 'https://picsum.photos/40/40?random=2',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 3,\n    title: 'Free Spirit',\n    artist: 'Khalid',\n    image: 'https://picsum.photos/40/40?random=3',\n    dateAdded: '2 day ago',\n    duration: '3:02',\n  },\n  {\n    id: 4,\n    title: 'Remind You',\n    artist: 'FRENSHIP',\n    image: 'https://picsum.photos/40/40?random=4',\n    dateAdded: '3 day ago',\n    duration: '4:25',\n  },\n]\n\nexport const BasicTable = {\n  render: () => (\n    <div style={{ width: '600px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Name</TableHeaderCell>\n            <TableHeaderCell>Email</TableHeaderCell>\n            <TableHeaderCell>Role</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell>John Doe</TableCell>\n            <TableCell>john@example.com</TableCell>\n            <TableCell>Admin</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Jane Smith</TableCell>\n            <TableCell>jane@example.com</TableCell>\n            <TableCell>User</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Bob Johnson</TableCell>\n            <TableCell>bob@example.com</TableCell>\n            <TableCell>Editor</TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n\nexport const EmptyTable = {\n  render: () => (\n    <div style={{ width: '500px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Column&nbsp;1</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;2</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;3</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell colSpan={3}>\n              <Typography variant=\"body2\" style={{ textAlign: 'center', padding: '40px 20px' }}>\n                No data available\n              </Typography>\n            </TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Table/Table.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport s from './Table.module.css'\n\n/*\n * Table\n */\n\nexport type TableProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'table'>\n\nexport const Table = ({ children, className, ...props }: TableProps) => {\n  return (\n    <table className={clsx(s.table, className)} {...props}>\n      {children}\n    </table>\n  )\n}\n\n/*\n * TableHead\n */\n\nexport type TableHeadProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'thead'>\n\nexport const TableHead = ({ children, className, ...props }: TableHeadProps) => {\n  return (\n    <thead className={clsx(s.tableHead, className)} {...props}>\n      {children}\n    </thead>\n  )\n}\n\n/*\n * TableBody\n */\n\nexport type TableBodyProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tbody'>\n\nexport const TableBody = ({ children, className, ...props }: TableBodyProps) => {\n  return (\n    <tbody className={clsx(s.tableBody, className)} {...props}>\n      {children}\n    </tbody>\n  )\n}\n\n/*\n * TableRow\n */\n\nexport type TableRowProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tr'>\n\nexport const TableRow = ({ children, className, ...props }: TableRowProps) => {\n  return (\n    <tr className={clsx(s.tableRow, className)} {...props}>\n      {children}\n    </tr>\n  )\n}\n\n/*\n * TableHeaderCell\n */\n\nexport type TableHeaderCellProps = {\n  children?: ReactNode\n  className?: string\n} & ComponentProps<'th'>\n\nexport const TableHeaderCell = ({ children, className, ...props }: TableHeaderCellProps) => {\n  return (\n    <th className={clsx(s.tableHeaderCell, className)} {...props}>\n      {children}\n    </th>\n  )\n}\n\n/*\n * TableCell\n */\n\nexport type TableCellProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'td'>\n\nexport const TableCell = ({ children, className, ...props }: TableCellProps) => {\n  return (\n    <td className={clsx(s.tableCell, className)} {...props}>\n      {children}\n    </td>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Table/index.ts",
    "content": "export * from './Table'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Tabs/Tabs.module.css",
    "content": ".tabsList {\n  display: flex;\n  width: 100%;\n  border-bottom: 1px solid var(--color-text-secondary);\n}\n\n.tabsTrigger {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  flex: 1 1 0;\n  align-items: center;\n  justify-content: center;\n\n  padding: 12px 16px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.tabsTrigger:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.tabsTrigger:not(.active, :disabled):hover {\n  opacity: 0.7;\n}\n\n.tabsTrigger.active {\n  color: var(--color-accent);\n}\n\n.tabsTrigger.active::after {\n  content: '';\n\n  position: absolute;\n  bottom: -1px;\n  left: 0;\n\n  width: 100%;\n  height: 2px;\n\n  background-color: var(--color-accent);\n}\n\n.tabsTrigger.disabled {\n  cursor: default;\n  color: var(--color-disabled);\n}\n\n.tabsContent {\n  padding: 32px 0;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Tabs/Tabs.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs'\n\nconst meta = {\n  title: 'Components/Tabs',\n  component: Tabs,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Tabs>\n\nexport default meta\n\nexport const BasicTabs = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Tabs defaultValue=\"account\">\n        <TabsList>\n          <TabsTrigger value=\"account\">Account</TabsTrigger>\n          <TabsTrigger value=\"password\">Password</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"account\">\n          <Typography variant=\"body1\">Make changes to your account here.</Typography>\n        </TabsContent>\n        <TabsContent value=\"password\">\n          <Typography variant=\"body1\">Change your password here.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n\nexport const ControlledTabs = {\n  render: () => {\n    const [activeTab, setActiveTab] = useState('tab1')\n\n    return (\n      <div style={{ width: '500px' }}>\n        <Tabs value={activeTab} onValueChange={setActiveTab}>\n          <TabsList>\n            <TabsTrigger value=\"tab1\">Tab 1</TabsTrigger>\n            <TabsTrigger value=\"tab2\">Tab 2</TabsTrigger>\n            <TabsTrigger value=\"tab3\">Tab 3</TabsTrigger>\n          </TabsList>\n          <TabsContent value=\"tab1\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              First Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the first tab. You can put any React content here.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab2\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Second Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the second tab with different information.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab3\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Third Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              And this is the third tab with its own unique content.\n            </Typography>\n          </TabsContent>\n        </Tabs>\n\n        <Card\n          style={{\n            marginTop: '20px',\n          }}>\n          <Typography variant=\"body2\">\n            Active tab: <strong>{activeTab}</strong>\n          </Typography>\n          <div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab1')}>\n              Go to Tab 1\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab2')}>\n              Go to Tab 2\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab3')}>\n              Go to Tab 3\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const DisabledTab = {\n  render: () => (\n    <div style={{ width: '350px' }}>\n      <Tabs defaultValue=\"available\">\n        <TabsList>\n          <TabsTrigger value=\"available\">Available</TabsTrigger>\n          <TabsTrigger value=\"disabled\" disabled>\n            Disabled\n          </TabsTrigger>\n          <TabsTrigger value=\"another\">Another</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"available\">\n          <Typography variant=\"body1\">This tab is available and active.</Typography>\n        </TabsContent>\n        <TabsContent value=\"disabled\">\n          <Typography variant=\"body1\">This content should not be visible.</Typography>\n        </TabsContent>\n        <TabsContent value=\"another\">\n          <Typography variant=\"body1\">This is another available tab.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Tabs/Tabs.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ComponentProps, createContext, type ReactNode, use, useState } from 'react'\n\nimport s from './Tabs.module.css'\n\ntype TabsContextType = {\n  value?: string\n  onValueChange?: (value: string) => void\n}\n\nconst TabsContext = createContext<TabsContextType | null>(null)\n\nconst useTabsContext = () => {\n  const context = use(TabsContext)\n  if (!context) {\n    throw new Error('Tabs compound components must be used within Tabs component')\n  }\n  return context\n}\n\n/*\n * Tabs\n */\n\nexport type TabsProps = {\n  children: ReactNode\n  defaultValue?: string\n  value?: string\n  onValueChange?: (value: string) => void\n} & ComponentProps<'div'>\n\nexport const Tabs = ({\n  children,\n  defaultValue,\n  value: controlledValue,\n  onValueChange,\n  className,\n  ...props\n}: TabsProps) => {\n  const [internalValue, setInternalValue] = useState(defaultValue)\n\n  const isControlled = controlledValue !== undefined\n  const value = isControlled ? controlledValue : internalValue\n\n  const handleValueChange = (newValue: string) => {\n    if (!isControlled) {\n      setInternalValue(newValue)\n    }\n    onValueChange?.(newValue)\n  }\n\n  return (\n    <div className={className} {...props}>\n      <TabsContext value={{ value, onValueChange: handleValueChange }}>{children}</TabsContext>\n    </div>\n  )\n}\n\n/*\n * TabsList\n */\n\nexport type TabsListProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const TabsList = ({ children, className }: TabsListProps) => {\n  return <div className={clsx(s.tabsList, className)}>{children}</div>\n}\n\n/*\n * TabsTrigger\n */\n\nexport type TabsTriggerProps = {\n  children: ReactNode\n  value: string\n  className?: string\n  disabled?: boolean\n}\n\nexport const TabsTrigger = ({ children, value, className, disabled }: TabsTriggerProps) => {\n  const { value: activeValue, onValueChange } = useTabsContext()\n  const isActive = activeValue === value\n\n  const handleClick = () => {\n    if (!disabled) {\n      onValueChange?.(value)\n    }\n  }\n\n  return (\n    <button\n      className={clsx(s.tabsTrigger, isActive && s.active, disabled && s.disabled, className)}\n      onClick={handleClick}\n      disabled={disabled}\n      type=\"button\">\n      {children}\n    </button>\n  )\n}\n\n/*\n * TabsContent\n */\n\nexport type TabsContentProps = {\n  children: ReactNode\n  value: string\n  className?: string\n}\n\nexport const TabsContent = ({ children, value, className }: TabsContentProps) => {\n  const { value: activeValue } = useTabsContext()\n  const isActive = activeValue === value\n\n  if (!isActive) return null\n\n  return <div className={clsx(s.tabsContent, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Tabs/index.ts",
    "content": "export * from './Tabs'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/TagEditor/TagEditor.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n}\n\n.tagsContainer {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n\n  margin-top: 12px;\n  padding: 8px 0;\n}\n\n.tag {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n\n  padding: 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 16px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.deleteButton:enabled:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.counter {\n  margin-top: 8px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/TagEditor/TagEditor.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { TagEditor } from './TagEditor'\n\nconst meta = {\n  title: 'Components/TagEditor',\n  component: TagEditor,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TagEditor>\n\nexport default meta\n\nexport const Basic = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags\"\n          placeholder=\"Add tag and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Skills (max 5)\"\n          placeholder=\"Add skill and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={5}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [tags, setTags] = useState(['React', 'TypeScript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags (disabled)\"\n          placeholder=\"Cannot add tags\"\n          value={tags}\n          onTagsChange={setTags}\n          disabled={true}\n        />\n      </div>\n    )\n  },\n}\n\nexport const PrefilledTags = {\n  render: () => {\n    const [tags, setTags] = useState([\n      'JavaScript',\n      'TypeScript',\n      'React',\n      'Node.js',\n      'CSS',\n      'HTML',\n    ])\n\n    return (\n      <div style={{ width: '450px' }}>\n        <TagEditor\n          label=\"Programming Languages & Technologies\"\n          placeholder=\"Add more technologies...\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={10}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendTags, setFrontendTags] = useState(['React', 'Vue.js'])\n    const [backendTags, setBackendTags] = useState(['Node.js'])\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Frontend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Frontend\"\n            placeholder=\"Add frontend technology...\"\n            value={frontendTags}\n            onTagsChange={setFrontendTags}\n            maxTags={8}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Backend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Backend\"\n            placeholder=\"Add backend technology...\"\n            value={backendTags}\n            onTagsChange={setBackendTags}\n            maxTags={6}\n          />\n        </div>\n\n        <Card>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Summary:\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '4px' }}>\n            Frontend: {frontendTags.length > 0 ? frontendTags.join(', ') : 'None'}\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block' }}>\n            Backend: {backendTags.length > 0 ? backendTags.join(', ') : 'None'}\n          </Typography>\n        </Card>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/TagEditor/TagEditor.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, KeyboardEvent } from 'react'\nimport { useState } from 'react'\n\nimport { DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport s from './TagEditor.module.css'\n\nexport type TagEditorProps = {\n  label?: string\n  placeholder?: string\n  value: string[]\n  onTagsChange: (tags: string[]) => void\n  maxTags?: number\n  disabled?: boolean\n} & ComponentProps<'div'>\n\nexport const TagEditor = ({\n  label,\n  placeholder = 'Add tag and press Enter',\n  value,\n  onTagsChange,\n  className,\n  maxTags,\n  disabled = false,\n  ...props\n}: TagEditorProps) => {\n  const [inputValue, setInputValue] = useState('')\n\n  const addTag = (tag: string) => {\n    const trimmedTag = tag.trim()\n\n    if (!trimmedTag) return\n    if (value.includes(trimmedTag)) return\n    if (maxTags && value.length >= maxTags) return\n\n    onTagsChange([...value, trimmedTag])\n    setInputValue('')\n  }\n\n  const removeTag = (tagToRemove: string) => {\n    onTagsChange(value.filter((tag) => tag !== tagToRemove))\n  }\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      addTag(inputValue)\n    }\n\n    if (e.key === 'Backspace' && !inputValue && value.length > 0) {\n      removeTag(value[value.length - 1])\n    }\n  }\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <TextField\n        label={label}\n        value={inputValue}\n        onChange={(e) => setInputValue(e.target.value)}\n        onKeyDown={handleKeyDown}\n        placeholder={isMaxTagsReached ? 'Max tags reached' : placeholder}\n        disabled={disabled}\n      />\n\n      {value.length > 0 && (\n        <ul className={s.tagsContainer}>\n          {value.map((tag) => (\n            <li key={tag} className={s.tag}>\n              <Typography variant=\"body2\" className={s.tagText}>\n                {tag}\n              </Typography>\n              <IconButton\n                onClick={() => removeTag(tag)}\n                className={s.deleteButton}\n                disabled={disabled}\n                aria-label={`Remove tag ${tag}`}\n                type=\"button\">\n                <DeleteIcon />\n              </IconButton>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} tags\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/TagEditor/index.ts",
    "content": "export * from './TagEditor'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/TextField/TextField.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.icon {\n  position: absolute;\n  top: 50%;\n  left: 12px;\n  transform: translateY(-50%);\n\n  display: flex;\n\n  color: var(--color-text-secondary);\n}\n\n.input {\n  width: 100%;\n  height: 40px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input.large {\n  height: 56px;\n}\n\n.input:disabled {\n  color: var(--color-disabled);\n}\n\n.input:focus,\n.input:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.input:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input.error {\n  border-color: var(--color-text-error);\n}\n\n.input.withIcon {\n  padding-left: 40px;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/TextField/TextField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport { TextField } from './TextField'\n\nconst meta = {\n  title: 'Components/TextField',\n  component: TextField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TextField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const Search: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    icon: <SearchIcon width={20} height={20} />,\n    inputSize: 'l',\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/TextField/TextField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './TextField.module.css'\n\nexport type TextFieldSize = 'm' | 'l'\n\nexport type TextFieldProps = {\n  errorMessage?: string\n  label?: ReactNode\n  icon?: ReactNode\n  inputSize?: TextFieldSize\n} & ComponentProps<'input'>\n\nexport const TextField = ({\n  className,\n  errorMessage,\n  id,\n  icon,\n  label,\n  inputSize = 'm',\n  ...props\n}: TextFieldProps) => {\n  const showError = Boolean(errorMessage)\n  const inputId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={inputId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.inputWrapper}>\n        {icon && <span className={s.icon}>{icon}</span>}\n        <input\n          className={clsx(\n            s.input,\n            showError && s.error,\n            icon && s.withIcon,\n            inputSize === 'l' && s.large\n          )}\n          id={inputId}\n          type={'text'}\n          {...props}\n        />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/TextField/index.ts",
    "content": "export * from './TextField'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Textarea/Textarea.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.textarea {\n  resize: none;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.textarea:disabled {\n  color: var(--color-disabled);\n}\n\n.textarea:focus,\n.textarea:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.textarea:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.textarea::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.textarea.error {\n  border-color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Textarea/Textarea.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Textarea } from './Textarea'\n\nconst meta = {\n  title: 'Components/Textarea',\n  component: Textarea,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Textarea>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const WithRows: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    rows: 5,\n  },\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Textarea/Textarea.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Textarea.module.css'\n\nexport type TextareaProps = {\n  errorMessage?: string\n  label?: ReactNode\n} & ComponentProps<'textarea'>\n\nexport const Textarea = ({ className, errorMessage, id, label, ...props }: TextareaProps) => {\n  const showError = Boolean(errorMessage)\n  const textareaId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={textareaId}>\n          {label}\n        </Typography>\n      )}\n\n      <textarea className={clsx(s.textarea, showError && s.error)} id={textareaId} {...props} />\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Textarea/index.ts",
    "content": "export * from './Textarea'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Typography/Typography.module.css",
    "content": ".label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.error {\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.h1 {\n  font-size: var(--font-size-xxxl);\n}\n\n.h2 {\n  margin: 0;\n  font-size: var(--font-size-xl);\n  font-weight: 600;\n  line-height: 1.3;\n}\n\n.h3 {\n  margin: 0;\n  font-size: var(--font-size-xs);\n  font-weight: 600;\n  line-height: 1.7;\n}\n\n.body1 {\n  margin: 0;\n  font-size: var(--font-size-l);\n  font-weight: 400;\n}\n\n.body2 {\n  margin: 0;\n  font-size: var(--font-size-m);\n  font-weight: 400;\n  color: var(--color-text-secondary);\n}\n\n.body3 {\n  margin: 0;\n  font-size: var(--font-size-xxs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* ------------------------------------------------------------ */\n\n.caption {\n  margin: 0;\n  font-size: 0.75rem;\n  font-weight: 400;\n  line-height: 1.66;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Typography/Typography.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from './Typography'\n\nconst meta = {\n  title: 'Components/Typography',\n  component: Typography,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Typography>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllTypography: Story = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>\n      <Typography variant=\"h1\">h1</Typography>\n      <Typography variant=\"h2\">h2</Typography>\n      <Typography variant=\"h3\">h3</Typography>\n      <Typography variant=\"body1\">body1</Typography>\n      <Typography variant=\"body2\">body2</Typography>\n      <Typography variant=\"caption\">caption</Typography>\n      <Typography variant=\"label\">label</Typography>\n      <Typography variant=\"error\">error</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Typography/Typography.tsx",
    "content": "import clsx from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\nimport React from 'react'\n\nimport styles from './Typography.module.css'\n\nconst VARIANT_DEFAULT_COMPONENT: Record<string, ElementType> = {\n  h1: 'h1',\n  h2: 'h2',\n  h3: 'h3',\n  body1: 'p',\n  body2: 'p',\n  body3: 'p',\n  caption: 'span',\n  label: 'label',\n}\n\ntype TypographyVariant =\n  | 'h1'\n  | 'h2'\n  | 'h3'\n  | 'body1'\n  | 'body2'\n  | 'body3'\n  | 'caption'\n  | 'label'\n  | 'error'\n\ntype Props<T extends ElementType> = {\n  variant?: TypographyVariant\n  as?: T\n  children: React.ReactNode\n} & ComponentProps<T>\n\nexport const Typography = <T extends ElementType = 'span'>({\n  variant = 'body1',\n  as,\n  children,\n  className = '',\n  ...props\n}: Props<T>) => {\n  const Component = as || VARIANT_DEFAULT_COMPONENT[variant] || 'span'\n  const variantClass = styles[variant] || ''\n\n  return (\n    <Component className={clsx(variantClass, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/Typography/index.ts",
    "content": "export * from './Typography'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/components/index.ts",
    "content": "export * from './AudioPlayer'\nexport * from './Autocomplete'\nexport * from './Button'\nexport * from './Card'\nexport * from './Dialog'\nexport * from './DropdownMenu'\nexport * from './Hashtag'\nexport * from './IconButton'\nexport * from './ImageUploader'\nexport * from './Pagination'\nexport * from './Progress'\nexport * from './ReactionButtons'\nexport * from './SearchField'\nexport * from './Select'\nexport * from './Table'\nexport * from './Tabs'\nexport * from './TagEditor'\nexport * from './Textarea'\nexport * from './TextField'\nexport * from './Typography'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/config/config.ts",
    "content": "export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL\nexport const API_KEY = import.meta.env.VITE_API_KEY\nexport const CURRENT_APP_DOMAIN = import.meta.env.VITE_BASE_URL\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/hooks/index.ts",
    "content": "export * from './useDebounceValue'\nexport * from './useGetId'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/hooks/useDebounceValue.ts",
    "content": "import { useEffect, useState } from 'react'\n\nexport const useDebounceValue = <T>(value: T, delay: number = 700): [T] => {\n  const [debounced, setDebounced] = useState(value)\n\n  useEffect(() => {\n    const handler = setTimeout(() => setDebounced(value), delay)\n    return () => clearTimeout(handler)\n  }, [value, delay])\n\n  return [debounced]\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/hooks/useGetId.ts",
    "content": "import { useId } from 'react'\n\n/*\n * Custom hook to get an ID.\n * If an ID is passed from component props, it returns that ID.\n * Otherwise, it generates and returns a new unique ID.\n *\n * @param {string} [idFromComponentProps] - An optional ID passed from ComponentProps.\n * @returns {string} The ID from component props or a generated unique ID.\n */\nexport const useGetId = (idFromComponentProps?: string) => {\n  const generatedId = useId()\n\n  return idFromComponentProps || generatedId\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/AddToPlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const AddToPlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <path\n      fill=\"#000\"\n      d=\"M8.134 4.795v2.456h2.34v.776h-2.34V10.5h-.84V8.026H4.966v-.776h2.328V4.795h.84Z\"\n    />\n    <path\n      fill=\"#fff\"\n      d=\"M5.89 16.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.665 2.333 2.333 0 0 0 0-4.665ZM17.89 14.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.666 2.333 2.333 0 0 0 0-4.666ZM10.902 5.9l10.489-1.998v1l-10.5 2 .011-1.003Z\"\n    />\n    <path fill=\"#fff\" d=\"M8.39 11.5h1v8l-1-.533V11.5ZM20.39 4.964l1-.464v13l-1-.928V4.963Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/ArrowDownIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ArrowDownIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={20}\n    height={20}\n    viewBox=\"0 0 20 20\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M6.175 7.158 10 10.975l3.825-3.817L15 8.333l-5 5-5-5 1.175-1.175Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/ClockIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ClockIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    viewBox=\"0 0 28 28\"\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14 3c6.075 0 11 4.925 11 11s-4.925 11-11 11S3 20.075 3 14 7.925 3 14 3Zm0 2a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm.5 8.5H18v2h-5.5v-7h2v5Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M0 0h28v28H0z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/CreateIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const CreateIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M16 2.667C8.64 2.667 2.667 8.64 2.667 16S8.64 29.333 16 29.333 29.333 23.36 29.333 16 23.36 2.666 16 2.666Zm6.667 14.666h-5.334v5.334h-2.666v-5.334H9.333v-2.666h5.334V9.332h2.666v5.333h5.334v2.667Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/DeleteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DeleteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={10}\n    height={12}\n    viewBox=\"0 0 10 12\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M7.333 4.25v5.833H2.666V4.25h4.667ZM6.458.75H3.54l-.583.583H.916V2.5h8.167V1.333H7.04L6.458.75Zm2.041 2.333h-7v7a1.17 1.17 0 0 0 1.167 1.167h4.667a1.17 1.17 0 0 0 1.166-1.167v-7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/DislikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DislikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 28 28\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-1.12 0-2.217.292-3.185.805L14 10.5h3.5L14 22.167l1.167-10.5h-3.5l1.796-6.289C12.215 4.212 10.512 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.818 4.854 8.376 11.667 14.583 6.382-5.763 11.667-9.637 11.667-14.583 0-3.594-2.824-6.417-6.417-6.417Zm-7.303 16.018c-4.422-3.955-7.28-6.685-7.28-9.601A4.044 4.044 0 0 1 8.75 5.833c.688 0 1.388.175 2.018.49L8.575 14h3.99l-.618 5.518Zm5.705-1.4 2.986-9.951h-3.395l.712-2.124c.42-.14.863-.21 1.295-.21a4.044 4.044 0 0 1 4.083 4.084c0 2.578-2.356 5.168-5.681 8.201Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/DownloadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DownloadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={22}\n    height={22}\n    viewBox=\"0 0 22 22\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M11.733 14.164V5.867h-1.466v8.286l-2.822-3.28-1.112.954 4.668 5.43 4.687-5.427-1.112-.958-2.843 3.292ZM11 0C4.925 0 0 4.925 0 11s4.925 11 11 11 11-4.925 11-11S17.075 0 11 0Zm0 20.533c-5.257 0-9.533-4.277-9.533-9.533 0-5.257 4.276-9.533 9.533-9.533 5.256 0 9.533 4.276 9.533 9.533 0 5.256-4.277 9.533-9.533 9.533Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/EditIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const EditIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m13.888 9.517.844.766-8.305 7.55h-.844v-.766l8.305-7.55Zm3.3-5.017a.966.966 0 0 0-.641.242l-1.678 1.525 3.438 3.125 1.677-1.525a.778.778 0 0 0 0-1.175l-2.145-1.95a.949.949 0 0 0-.65-.242Zm-3.3 2.658L3.75 16.375V19.5h3.438l10.138-9.217-3.438-3.125Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/HomeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const HomeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      d=\"M16.0001 7.58667L22.6667 13.5867V24H20.0001V16H12.0001V24H9.33341V13.5867L16.0001 7.58667ZM16.0001 4L2.66675 16H6.66675V26.6667H14.6667V18.6667H17.3334V26.6667H25.3334V16H29.3334L16.0001 4Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/ImageUploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ImageUploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={35}\n    height={34}\n    fill=\"none\"\n    viewBox=\"0 0 35 34\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M30.834 3.667v20h-20v-20h20Zm0-3.334h-20a3.343 3.343 0 0 0-3.333 3.334v20C7.5 25.5 9 27 10.834 27h20c1.833 0 3.333-1.5 3.333-3.333v-20c0-1.834-1.5-3.334-3.333-3.334ZM16.667 16.45l2.817 3.767 4.133-5.167 5.55 6.95H12.501l4.166-5.55ZM.834 7v23.333c0 1.834 1.5 3.334 3.333 3.334h23.334v-3.334H4.167V7H.834Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/KeyboardArrowLeftIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowLeftIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path fill=\"currentColor\" d=\"M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/KeyboardArrowRightIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowRightIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width={24} height={24} fill=\"none\" {...props}>\n    <path fill=\"#fff\" d=\"M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/LibraryIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LibraryIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"  currentColor\"\n      d=\"M26.667 2.667h-16A2.674 2.674 0 0 0 8 5.332v16C8 22.8 9.2 24 10.667 24h16c1.466 0 2.666-1.2 2.666-2.667v-16c0-1.467-1.2-2.667-2.666-2.667Zm0 16.666a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12Zm-10 .667A3.335 3.335 0 0 0 20 16.666v-5.333a2 2 0 0 1 2-2h.667a1.333 1.333 0 1 0 0-2.667h-2a2 2 0 0 0-2 2v3.196c0 .882-1.119 1.471-2 1.471a3.334 3.334 0 0 0 0 6.667ZM5.333 9.333a1.333 1.333 0 1 0-2.666 0v17.333c0 1.467 1.2 2.667 2.666 2.667h17.334a1.333 1.333 0 0 0 0-2.666H7.333a2 2 0 0 1-2-2V9.332Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/LikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    viewBox=\"0 0 28 28\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-2.03 0-3.978.945-5.25 2.438C12.728 4.445 10.78 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.41 3.967 8.003 9.975 13.463L14 24.908l1.692-1.54c6.008-5.448 9.975-9.041 9.975-13.451 0-3.594-2.824-6.417-6.417-6.417Zm-5.133 18.142-.117.116-.117-.116C8.33 16.613 4.667 13.288 4.667 9.917c0-2.334 1.75-4.084 4.083-4.084 1.797 0 3.547 1.155 4.165 2.754h2.182c.606-1.599 2.356-2.754 4.153-2.754 2.333 0 4.083 1.75 4.083 4.084 0 3.371-3.663 6.696-9.216 11.725Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/LikeIconFill.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIconFill = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 29 28\"\n    width={29}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14.4 6.04a6.137 6.137 0 0 1 8.655.248c2.375 2.47 2.457 6.402.247 8.967L14.4 24.5l-8.902-9.245c-2.21-2.566-2.126-6.504.248-8.967C8.123 3.823 11.927 3.74 14.4 6.04Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M.4 0h28v28H.4z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/LikeInSquareIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeInSquareIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <rect width={32} height={32} fill=\"url(#a)\" rx={2} />\n    <path\n      fill=\"#fff\"\n      d=\"M16 10.158c1.645-1.597 4.186-1.544 5.77.173 1.583 1.717 1.638 4.453.165 6.237L16 23l-5.934-6.432c-1.473-1.784-1.418-4.524.165-6.237 1.585-1.715 4.121-1.773 5.77-.173Z\"\n    />\n    <defs>\n      <linearGradient id=\"a\" x1={1} x2={32} y1={1} y2={30.5} gradientUnits=\"userSpaceOnUse\">\n        <stop stopColor=\"#3822EA\" />\n        <stop offset={1} stopColor=\"#C7E9D7\" />\n      </linearGradient>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/LiveWaveIcon/LiveWaveIcon.module.css",
    "content": ".bar {\n  transform-origin: center bottom;\n  animation: wave 1.2s ease-in-out infinite alternate;\n}\n\n@keyframes wave {\n  0% {\n    transform: scaleY(0.4);\n  }\n\n  50% {\n    transform: scaleY(1);\n  }\n\n  100% {\n    transform: scaleY(0.6);\n  }\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/LiveWaveIcon/LiveWaveIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nimport s from './LiveWaveIcon.module.css'\n\nexport const LiveWaveIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <rect\n      x={2}\n      y={8}\n      width={2}\n      height={8}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '0ms' }}\n    />\n    <rect\n      x={6}\n      y={4}\n      width={2}\n      height={16}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '150ms' }}\n    />\n    <rect\n      x={10}\n      y={6}\n      width={2}\n      height={12}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '300ms' }}\n    />\n    <rect\n      x={14}\n      y={2}\n      width={2}\n      height={20}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '450ms' }}\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/LiveWaveIcon/index.ts",
    "content": "export { LiveWaveIcon } from './LiveWaveIcon'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/LogoutIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const LogoutIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m17 8-1.41 1.41L17.17 11H9v2h8.17l-1.58 1.58L17 16l4-4-4-4ZM5 5h7V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h7v-2H5V5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/MoreIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const MoreIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={16}\n    height={4}\n    viewBox=\"0 0 16 4\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M2 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM8 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM16 2a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/PauseIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PauseIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 40 40\"\n    width={40}\n    height={40}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"#fff\"\n      d=\"M20 0c11.046 0 20 8.954 20 20s-8.954 20-20 20S0 31.046 0 20 8.954 0 20 0Zm-6 11a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Zm9 0a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/PlayIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlayIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={72}\n    height={72}\n    viewBox=\"0 0 72 72\"\n    fill=\"none\"\n    {...props}>\n    <circle cx={36} cy={36} r={36} fill=\"#FF38B6\" />\n    <path\n      fill=\"#000\"\n      d=\"M49.287 36.512c.865-.486.865-1.7 0-2.186l-19.47-10.93c-.864-.485-1.946.122-1.946 1.093v21.86c0 .971 1.082 1.579 1.947 1.093l19.469-10.93Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/PlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M28 4H4a2.675 2.675 0 0 0-2.667 2.667v18.666C1.333 26.8 2.533 28 4 28h24c1.467 0 2.667-1.2 2.667-2.667V6.667C30.667 5.2 29.467 4 28 4Zm0 21.333H4V6.667h24v18.666ZM10.667 20c0-2.213 1.786-4 4-4 .466 0 .92.093 1.333.24V8h6.667v2.667h-4v9.373a4.003 4.003 0 0 1-4 3.96c-2.214 0-4-1.787-4-4Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/PlusIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlusIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <path\n      fill=\"var(--color-text-secondary)\"\n      d=\"M30 0a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h28ZM15 9v6H9v2h6v6h2v-6h6v-2h-6V9h-2Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/ProfileIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const ProfileIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2Zm0 16h-4.83L12 20.17 9.83 18H5V4h14v14Zm-7-7c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3Zm0-4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1Zm6 8.58c0-2.5-3.97-3.58-6-3.58s-6 1.08-6 3.58V17h12v-1.42ZM8.48 15c.74-.51 2.23-1 3.52-1s2.78.49 3.52 1H8.48Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/RepeatIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const RepeatIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M9.333 9.333h13.334v4L28 8l-5.333-5.333v4h-16v8h2.666V9.332Zm13.334 13.333H9.333v-4L4 24l5.333 5.333v-4h16v-8h-2.666v5.334Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/SearchIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SearchIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 32 32\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m23.775 22.356 5.817 6.137c.56.59.541 1.534-.04 2.1a1.414 1.414 0 0 1-2.024-.04l-5.822-6.145c-1.979 1.522-4.21 2.36-6.695 2.512a11.872 11.872 0 0 1-4.822-.691c-1.556-.563-2.912-1.366-4.07-2.41-1.159-1.042-2.107-2.313-2.843-3.813a12.37 12.37 0 0 1-1.254-4.779 12.41 12.41 0 0 1 .687-4.898c.557-1.58 1.35-2.958 2.378-4.136 1.028-1.177 2.281-2.14 3.76-2.89a11.915 11.915 0 0 1 4.707-1.28c1.66-.102 3.268.129 4.823.692 1.555.563 2.912 1.366 4.07 2.409 1.159 1.043 2.106 2.314 2.843 3.814a12.368 12.368 0 0 1 1.253 4.779 12.567 12.567 0 0 1-.21 3.162 12.259 12.259 0 0 1-.958 2.929 12.892 12.892 0 0 1-1.6 2.548Zm-8.935 1.635a9.024 9.024 0 0 0 3.596-.982 9.525 9.525 0 0 0 2.869-2.216c.786-.9 1.394-1.952 1.823-3.156a9.4 9.4 0 0 0 .53-3.743 9.367 9.367 0 0 0-.963-3.65c-.566-1.143-1.292-2.113-2.178-2.91a9.443 9.443 0 0 0-3.106-1.847 8.992 8.992 0 0 0-3.685-.534 9.025 9.025 0 0 0-3.596.982A9.524 9.524 0 0 0 7.26 8.15c-.785.9-1.393 1.953-1.822 3.157a9.4 9.4 0 0 0-.53 3.742 9.367 9.367 0 0 0 .962 3.65c.567 1.144 1.293 2.114 2.179 2.91a9.443 9.443 0 0 0 3.106 1.848 8.994 8.994 0 0 0 3.685.534Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/ShuffleIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ShuffleIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M14.12 12.227 7.213 5.333l-1.88 1.88 6.893 6.894 1.894-1.88Zm5.213-6.894 2.72 2.72-16.72 16.734 1.88 1.88 16.733-16.72 2.72 2.72V5.334h-7.333Zm.44 12.547-1.88 1.88 4.173 4.173-2.733 2.734h7.333v-7.334l-2.72 2.72-4.173-4.173Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/SkipNextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipNextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"m8 24 11.333-8L8 8v16Zm2.667-10.853L14.707 16l-4.04 2.853v-5.706ZM21.333 8H24v16h-2.667V8Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/SkipPreviousIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipPreviousIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M8 8h2.667v16H8V8Zm4.667 8L24 24V8l-11.333 8Zm8.666 2.853L17.293 16l4.04-2.853v5.706Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/TextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M14.17 5 19 9.83V19H5V5h9.17Zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V9.83c0-.53-.21-1.04-.59-1.41l-4.83-4.83c-.37-.38-.88-.59-1.41-.59ZM7 15h10v2H7v-2Zm0-4h10v2H7v-2Zm0-4h7v2H7V7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/TrackIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TrackIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m16 4 .013 14.067a5.329 5.329 0 0 0-2.666-.734A5.335 5.335 0 0 0 8 22.667 5.335 5.335 0 0 0 13.347 28c2.96 0 5.32-2.387 5.32-5.333V9.333H24V4h-8Zm-2.653 21.333a2.674 2.674 0 0 1-2.667-2.666c0-1.467 1.2-2.667 2.667-2.667 1.466 0 2.666 1.2 2.666 2.667 0 1.466-1.2 2.666-2.666 2.666Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/UploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const UploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M24 20v4H8v-4H5.333v4c0 1.467 1.2 2.667 2.667 2.667h16c1.467 0 2.667-1.2 2.667-2.667v-4H24ZM9.333 12l1.88 1.88 3.454-3.44v10.894h2.666V10.44l3.454 3.44 1.88-1.88L16 5.333 9.333 12Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/VolumeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M4 12v8h5.333L16 26.667V5.333L9.333 12H4Zm9.333-.227v8.454l-2.893-2.894H6.667v-2.666h3.773l2.893-2.894ZM22 16a6 6 0 0 0-3.333-5.373V21.36A5.965 5.965 0 0 0 22 16ZM18.667 4.307v2.746C22.52 8.2 25.333 11.773 25.333 16c0 4.227-2.813 7.8-6.666 8.947v2.746C24.013 26.48 28 21.707 28 16S24.013 5.52 18.667 4.307Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/VolumeMuteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeMuteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width={24} height={24} {...props}>\n    <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n    <g fill=\"currentColor\">\n      <path d=\"M16.25 13.42c.15-.45.25-.92.25-1.42A4.5 4.5 0 0 0 14 7.97v3.2l2.25 2.25z\" />\n      <path d=\"M19 12c0 1.21-.31 2.34-.85 3.32l1.46 1.46A8.973 8.973 0 0 0 21 12c0-3.83-2.4-7.11-5.78-8.4-.59-.23-1.22.23-1.22.86v.19c0 .38.25.71.61.85C17.18 6.54 19 9.06 19 12zM2.1 3.51a.996.996 0 0 0 0 1.41L6.17 9H4c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71v-2.76l3.32 3.32c-.23.13-.47.24-.71.35-.37.16-.6.52-.6.91 0 .7.7 1.2 1.35.94.5-.2.99-.45 1.44-.73l2.28 2.28a.996.996 0 1 0 1.41-1.41L3.51 3.51a.996.996 0 0 0-1.41 0zM12 9.17V6.41c0-.89-1.08-1.34-1.71-.71l-.88.89L12 9.17z\" />\n    </g>\n  </svg>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/shared/icons/index.ts",
    "content": "export * from './AddToPlaylistIcon'\nexport * from './ArrowDownIcon'\nexport * from './ClockIcon'\nexport * from './CreateIcon'\nexport * from './DeleteIcon'\nexport * from './DislikeIcon'\nexport * from './DownloadIcon'\nexport * from './EditIcon'\nexport * from './HomeIcon'\nexport * from './ImageUploadIcon'\nexport * from './KeyboardArrowLeftIcon'\nexport * from './KeyboardArrowRightIcon'\nexport * from './LibraryIcon'\nexport * from './LikeIcon'\nexport * from './LikeIconFill'\nexport * from './LikeInSquareIcon'\nexport * from './LiveWaveIcon'\nexport * from './LogoutIcon'\nexport * from './MoreIcon'\nexport * from './PauseIcon'\nexport * from './PlayIcon'\nexport * from './PlaylistIcon'\nexport * from './PlusIcon'\nexport * from './ProfileIcon'\nexport * from './RepeatIcon'\nexport * from './SearchIcon'\nexport * from './ShuffleIcon'\nexport * from './SkipNextIcon'\nexport * from './SkipPreviousIcon'\nexport * from './TextIcon'\nexport * from './TrackIcon'\nexport * from './UploadIcon'\nexport * from './VolumeIcon'\nexport * from './VolumeMuteIcon'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/index.ts",
    "content": "export { Layout } from './ui/Layout.tsx'\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/ui/Header/Header.module.css",
    "content": ".header {\n  display: flex;\n  grid-area: header;\n  align-items: center;\n  justify-content: space-between;\n\n  height: var(--header-height);\n  padding: 0 32px;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/ui/Header/Header.tsx",
    "content": "import { useUnit } from 'effector-react/effector-react.mjs'\n\nimport { LoginButtonAndModal, ProfileDropdownMenu } from '@/features/auth'\nimport { $isAuthorized } from '@/features/auth/model/model.ts'\n\nimport s from './Header.module.css'\n\nexport const Header = () => {\n  const isAuthorized = useUnit($isAuthorized)\n  return (\n    <header className={s.header}>\n      <div className={s.logo}>Musicfun</div>\n      {isAuthorized ? (\n        <ProfileDropdownMenu avatar={'//unsplash.it/100/100'} />\n      ) : (\n        <LoginButtonAndModal />\n      )}\n    </header>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/ui/Layout.module.css",
    "content": ".grid {\n  display: grid;\n  grid-template: 'header header' var(--header-height) 'sidebar main' 1fr / 310px 1fr;\n  height: 100vh;\n}\n\n.grid.playerOpen {\n  grid-template:\n    'header header' var(--header-height)\n    'sidebar main' 1fr 'player player' var(--player-height) / 310px 1fr;\n}\n\n.main {\n  overflow-y: auto;\n  grid-area: main;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/ui/Layout.tsx",
    "content": "import clsx from 'clsx'\nimport { Outlet } from 'react-router'\n\n// import { Player } from '@/widgets/Player'\nimport { Header } from './Header/Header.tsx'\nimport s from './Layout.module.css'\nimport { Sidebar } from './Sidebar/Sidebar.tsx'\n\nexport const Layout = () => {\n  const IS_PLAYER_OPEN = false\n\n  return (\n    <div className={clsx(s.grid, IS_PLAYER_OPEN && s.playerOpen)}>\n      <Header />\n      <Sidebar />\n      <main className={s.main}>\n        <Outlet />\n      </main>\n      {/*{IS_PLAYER_OPEN && <Player />}*/}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/ui/Sidebar/MenuLinks/MenuLinks.module.css",
    "content": ".column {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list + .list {\n  padding-top: 20px;\n  border-top: 1px solid var(--color-bg-secondary);\n}\n\n.link {\n  all: unset;\n\n  cursor: pointer;\n\n  display: flex;\n  gap: 16px;\n  align-items: center;\n\n  width: fit-content;\n\n  font-size: var(--font-size-m);\n  font-weight: 700;\n  color: var(--color-text-secondary);\n\n  transition: color 0.2s ease;\n}\n\n.link:hover {\n  color: var(--color-text-primary);\n}\n\n.active {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/ui/Sidebar/MenuLinks/MenuLinks.tsx",
    "content": "import clsx from 'clsx'\nimport { NavLink } from 'react-router'\n\nimport { HomeIcon, LibraryIcon, PlaylistIcon, TrackIcon, UploadIcon } from '@/shared/icons'\nimport { CreateIcon } from '@/shared/icons/CreateIcon'\n\nimport s from './MenuLinks.module.css'\n\ntype MenuLink = {\n  to: string\n  icon: React.ReactNode\n  label: string\n}\n\ntype MenuButton = {\n  onClick: () => void\n  icon: React.ReactNode\n  label: string\n}\n\nconst mainLinks: MenuLink[] = [\n  {\n    to: '/',\n    icon: <HomeIcon width={32} height={32} />,\n    label: 'Home',\n  },\n  {\n    to: '/user/1',\n    icon: <LibraryIcon />,\n    label: 'Your Library',\n  },\n]\n\nconst createLinks: MenuLink[] = [\n  {\n    to: '/tracks',\n    icon: <TrackIcon />,\n    label: 'All Tracks',\n  },\n  {\n    to: '/playlists',\n    icon: <PlaylistIcon />,\n    label: 'All Playlists',\n  },\n]\n\nexport const MenuLinks = () => {\n  const actionButtons: MenuButton[] = [\n    {\n      onClick: () => {},\n      icon: <UploadIcon />,\n      label: 'Upload Track',\n    },\n    {\n      onClick: () => {},\n      icon: <CreateIcon />,\n      label: 'Create Playlist',\n    },\n  ]\n\n  return (\n    <nav className={s.column} aria-label=\"Main navigation\">\n      <ul className={s.list}>\n        {mainLinks.map((props) => (\n          <li key={props.to}>\n            <SidebarLink {...props} />\n          </li>\n        ))}\n      </ul>\n      <ul className={s.list}>\n        {actionButtons.map((props) => (\n          <li key={props.label}>\n            <SidebarButton {...props} />\n          </li>\n        ))}\n      </ul>\n      <ul className={s.list}>\n        {createLinks.map((props) => (\n          <li key={props.to}>\n            <SidebarLink {...props} />\n          </li>\n        ))}\n      </ul>\n    </nav>\n  )\n}\n\nconst SidebarLink = ({ to, icon, label }: MenuLink) => (\n  <NavLink to={to} className={({ isActive }) => clsx(s.link, isActive && s.active)}>\n    {icon}\n    {label}\n  </NavLink>\n)\n\nconst SidebarButton = ({ onClick, icon, label }: MenuButton) => (\n  <button onClick={onClick} className={s.link} type=\"button\">\n    {icon}\n    {label}\n  </button>\n)\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/ui/Sidebar/Sidebar.module.css",
    "content": ".sidebar {\n  overflow-y: auto;\n  display: flex;\n  grid-area: sidebar;\n  flex-direction: column;\n\n  height: calc(100vh - var(--header-height) - var(--player-height));\n  padding: 0 30px;\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/src/widgets/layout/ui/Sidebar/Sidebar.tsx",
    "content": "import { MenuLinks } from './MenuLinks/MenuLinks.tsx'\nimport s from './Sidebar.module.css'\n\nexport const Sidebar = () => {\n  return (\n    <div className={s.sidebar}>\n      <MenuLinks />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*\", \"**/.server/**/*\", \"**/.client/**/*\", \".react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"node\", \"vite/client\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"esModuleInterop\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "apps/react-effector-fsd/vite.config.ts",
    "content": "import path from 'node:path'\n\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\nimport tsconfigPaths from 'vite-tsconfig-paths'\n\nexport default defineConfig({\n  plugins: [\n    react({\n      babel: {\n        plugins: ['effector/babel-plugin'],\n      },\n    }),\n    tsconfigPaths(),\n  ],\n  base: '/effector',\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, 'src'),\n    },\n  },\n})\n"
  },
  {
    "path": "apps/react-native-expo/.gitignore",
    "content": "# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files\n\n# dependencies\nnode_modules/\n\n# Expo\n.expo/\ndist/\nweb-build/\nexpo-env.d.ts\n\n# Native\n.kotlin/\n*.orig.*\n*.jks\n*.p8\n*.p12\n*.key\n*.mobileprovision\n\n# Metro\n.metro-health-check*\n\n# debug\nnpm-debug.*\nyarn-debug.*\nyarn-error.*\n\n# macOS\n.DS_Store\n*.pem\n\n# local env files\n.env*.local\n\n# typescript\n*.tsbuildinfo\n\n# generated native folders\n/ios\n/android\n\n.env.production\n.env.development\n"
  },
  {
    "path": "apps/react-native-expo/.npmrc",
    "content": "public-hoist-pattern[]=@babel/*\npublic-hoist-pattern[]=react\npublic-hoist-pattern[]=react-native\n\n"
  },
  {
    "path": "apps/react-native-expo/app/(app)/_layout.tsx",
    "content": "import { Redirect, SplashScreen, Tabs, useRootNavigationState, useRouter } from 'expo-router'\nimport { ActivityIndicator, StyleSheet, View } from 'react-native'\nimport { GestureHandlerRootView } from 'react-native-gesture-handler'\n\nimport { useMeQuery } from '@/features/auth/model/api/hooks/use-me.query'\nimport { useAuthContext } from '@/features/auth/model/context/AuthContext'\nimport { COLORS } from '@/shared/styles/tokens'\nimport { IcAllPlaylist } from '@/shared/ui/Icons/navigation/IcAllPlaylist'\nimport { IcAllTracks } from '@/shared/ui/Icons/navigation/IcAllTracks'\nimport { IcHome } from '@/shared/ui/Icons/navigation/IcHome'\nimport { IcYourLibrary } from '@/shared/ui/Icons/navigation/IcYourLibrary'\n\nexport default function AppLayout() {\n  const rootState = useRootNavigationState()\n  const { isAuth } = useAuthContext()\n\n  if (!isAuth) return <Redirect href=\"/(auth)/login\" />\n\n  return (\n    <>\n      <Tabs\n        initialRouteName={'index'}\n        screenOptions={{\n          headerShown: false,\n          headerShadowVisible: false,\n          headerStyle: {\n            // shadowColor: 'transparent'\n          },\n          tabBarStyle: {\n            backgroundColor: COLORS.DARK.BACKGROUND_MAIN,\n            borderTopColor: 'transparent',\n          },\n          tabBarActiveTintColor: 'white',\n\n          tabBarInactiveTintColor: 'gray',\n        }}>\n        <Tabs.Screen\n          name=\"index\"\n          options={{\n            title: 'Home',\n            tabBarIcon: () => <IcHome />,\n          }}\n        />\n        <Tabs.Screen\n          name=\"playlists/playlists\"\n          options={{\n            title: 'Playlists',\n            tabBarIcon: () => <IcAllPlaylist />,\n          }}\n        />\n        <Tabs.Screen\n          name=\"tracks/tracks\"\n          options={{ title: 'Tracks', tabBarIcon: () => <IcAllTracks /> }}\n        />\n        <Tabs.Screen\n          name=\"library/library\"\n          options={{\n            title: 'Library',\n            tabBarIcon: () => <IcYourLibrary />,\n          }}\n        />\n      </Tabs>\n\n      {!rootState?.key && (\n        <View style={styles.overlay}>\n          <ActivityIndicator />\n        </View>\n      )}\n    </>\n  )\n}\n\nconst styles = StyleSheet.create({\n  overlay: {\n    ...StyleSheet.absoluteFillObject,\n    // justifyContent: 'center',\n    // alignItems: 'center',\n    // backgroundColor: COLORS.DARK.BACKGROUND_MAIN,\n    opacity: 0.7,\n  },\n})\n"
  },
  {
    "path": "apps/react-native-expo/app/(app)/index.tsx",
    "content": "import { StyleSheet, Text } from 'react-native'\nimport { SafeAreaView } from 'react-native-safe-area-context'\n\nimport { LogoutButton } from '@/features/auth/components/LogoutButton/LogoutButton'\nimport { useMeQuery } from '@/features/auth/model/api/hooks/use-me.query'\nimport { COLORS, GAPS } from '@/shared/styles/tokens'\n\nexport default function Home() {\n  const { data, isPending } = useMeQuery()\n  return (\n    <SafeAreaView style={styles.container}>\n      {data && <Text style={{ color: 'white' }}>{data.login}</Text>}\n\n      <LogoutButton />\n    </SafeAreaView>\n  )\n}\n\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n    backgroundColor: COLORS.DARK.BACKGROUND_MAIN,\n    justifyContent: 'center',\n    alignItems: 'center',\n    gap: GAPS.G8,\n  },\n})\n"
  },
  {
    "path": "apps/react-native-expo/app/(app)/library/library.tsx",
    "content": "import { Text, View } from 'react-native'\n\nexport default function Library() {\n  return (\n    <View>\n      <Text>Library</Text>\n    </View>\n  )\n}\n"
  },
  {
    "path": "apps/react-native-expo/app/(app)/playlists/playlists.tsx",
    "content": "import { Text, View } from 'react-native'\n\nexport default function Playlists() {\n  return (\n    <View>\n      <Text>all-playlist</Text>\n    </View>\n  )\n}\n"
  },
  {
    "path": "apps/react-native-expo/app/(app)/tracks/tracks.tsx",
    "content": "import { Text, View } from 'react-native'\n\nexport default function Tracks() {\n  return (\n    <View>\n      <Text>tracks</Text>\n    </View>\n  )\n}\n"
  },
  {
    "path": "apps/react-native-expo/app/(auth)/_layout.tsx",
    "content": "import { Stack } from 'expo-router'\n\nimport { COLORS } from '@/shared/styles/tokens'\n\nexport default function AuthLayout() {\n  return (\n    <Stack\n      screenOptions={{\n        headerShown: false,\n        headerTintColor: COLORS.DARK.BUTTON_MAIN_PINK_HOVER,\n      }}>\n      <Stack.Screen name=\"login\" />\n    </Stack>\n  )\n}\n"
  },
  {
    "path": "apps/react-native-expo/app/(auth)/login.tsx",
    "content": "import { router } from 'expo-router'\nimport * as WebBrowser from 'expo-web-browser'\nimport { useEffect } from 'react'\nimport { StyleSheet, Text, View } from 'react-native'\nimport { SafeAreaView } from 'react-native-safe-area-context'\n\nimport { LoginButton } from '@/features/auth/components/LoginButton/LoginButton'\nimport { useAuthContext } from '@/features/auth/model/context/AuthContext'\nimport { COLORS, GAPS } from '@/shared/styles/tokens'\nimport { Button } from '@/shared/ui/Button/Button'\nimport { IcSmile } from '@/shared/ui/Icons/screens/login/IcSmile'\n\nexport default function Login() {\n  const { isAuth } = useAuthContext()\n\n  const onPressSignUp = async () => {\n    await WebBrowser.openBrowserAsync('https://github.com/signup')\n  }\n  //\n  useEffect(() => {\n    if (isAuth) router.replace('/playlists/playlists')\n  }, [isAuth])\n\n  // const query = useQuery({\n  //   staleTime: 5 * 1000, // 10 seconds когда данные устарели, сделай снова запрос\n  //   // gcTime: 5 * 1000, // 5 seconds сколько хранить данные в кэше\n  //   refetchOnMount: true, //когда монтируется компонент, сделай запрос (выключено)\n  //   refetchOnWindowFocus: true, //когда фокус на окне, сделай запрос (выключено)\n  //   refetchOnReconnect: false, //когда мы были офлайн, но стали онлайн\n  //   queryKey: ['playlists'],\n  //   queryFn: async ({ signal }) => {\n  //     const response = await AuthPlaylistInstance.getPlaylist()\n  //     return response?.data\n  //   },\n  // })\n\n  return (\n    <SafeAreaView style={styles.container}>\n      <View style={styles.flexContainer}>\n        <View></View>\n        <View>\n          <View style={styles.smileContainer}>\n            <IcSmile />\n          </View>\n          <View style={styles.textContainer}>\n            <Text style={styles.title}>\n              Millions of Songs.{'\\n'}\n              Free on Musifun.\n            </Text>\n          </View>\n          <View style={styles.buttonContainer}>\n            <LoginButton />\n            <Button\n              variant={'gray'}\n              isFull\n              onPress={onPressSignUp}\n              title=\"Continue without Sign In\"\n            />\n          </View>\n        </View>\n      </View>\n    </SafeAreaView>\n  )\n}\n\nconst styles = StyleSheet.create({\n  container: {\n    paddingHorizontal: 16,\n    flex: 1,\n    width: '100%',\n    justifyContent: 'center',\n    alignItems: 'center',\n    backgroundColor: COLORS.DARK.BACKGROUND_MAIN,\n  },\n  buttonContainer: {\n    gap: GAPS.G27,\n    width: '100%',\n    alignItems: 'stretch',\n  },\n  smileContainer: {\n    marginBottom: 19,\n    width: '100%',\n    alignItems: 'center',\n  },\n  textContainer: {\n    marginBottom: 26,\n    paddingHorizontal: 30,\n    width: '100%',\n    justifyContent: 'center',\n  },\n  title: {\n    fontSize: 30,\n    // lineHeight: 10,\n    letterSpacing: 0,\n    fontWeight: 700,\n    textAlign: 'center',\n    color: COLORS.DARK.TEXT_MAIN_WHITE,\n  },\n  flexContainer: {\n    flexDirection: 'column',\n    height: '100%',\n    justifyContent: 'space-around',\n    width: '100%',\n  },\n})\n"
  },
  {
    "path": "apps/react-native-expo/app/_layout.tsx",
    "content": "import '../shared/api/api-root/api-root'\n\nimport { useFonts } from 'expo-font'\nimport { Redirect, SplashScreen, Stack } from 'expo-router'\nimport { StatusBar } from 'expo-status-bar'\nimport { useEffect } from 'react'\nimport { ActivityIndicator, View } from 'react-native'\nimport { GestureHandlerRootView } from 'react-native-gesture-handler'\nimport { SafeAreaProvider } from 'react-native-safe-area-context'\n\nimport { AuthContextProvider } from '@/features/auth/model/context/AuthContext'\nimport { ReactQueryProvider } from '@/shared/providers/reactQueryProviders/ReactQueryProviders'\nimport { COLORS } from '@/shared/styles/tokens'\n\nSplashScreen.preventAutoHideAsync().catch(() => {})\n\nexport default function RootLayout() {\n  const [fontsLoaded] = useFonts({\n    'Lato-Regular': require('../assets/fonts/Lato-Regular.ttf'),\n    'Lato-Bold': require('../assets/fonts/Lato-Bold.ttf'),\n    'Lato-Light': require('../assets/fonts/Lato-Light.ttf'),\n    'Lato-Thin': require('../assets/fonts/Lato-Thin.ttf'),\n  })\n\n  useEffect(() => {\n    if (fontsLoaded) SplashScreen.hideAsync()\n  }, [fontsLoaded])\n\n  if (!fontsLoaded) {\n    return (\n      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>\n        <ActivityIndicator />\n      </View>\n    )\n  }\n\n  return (\n    <ReactQueryProvider>\n      <AuthContextProvider>\n        <GestureHandlerRootView style={{ flex: 1 }}>\n          <StatusBar hidden={false} style=\"light\" />\n          <SafeAreaProvider>\n            <Stack\n              screenOptions={{\n                headerShown: false,\n                headerStyle: { backgroundColor: COLORS.DARK.BACKGROUND_MAIN },\n                headerTintColor: COLORS.DARK.BUTTON_MAIN_PINK_HOVER,\n              }}>\n              <Stack.Screen name=\"(app)\" options={{ autoHideHomeIndicator: false }} />\n              <Stack.Screen name=\"(auth)\" options={{ autoHideHomeIndicator: false }} />\n            </Stack>\n          </SafeAreaProvider>\n        </GestureHandlerRootView>\n      </AuthContextProvider>\n    </ReactQueryProvider>\n  )\n}\n"
  },
  {
    "path": "apps/react-native-expo/app.config.ts",
    "content": "import * as dotenv from 'dotenv'\n\nconst PROFILE =\n  process.env.EAS_BUILD_PROFILE ??\n  process.env.APP_ENV ??\n  (process.env.NODE_ENV === 'production' ? 'production' : 'development')\n\ndotenv.config({ path: `.env.${PROFILE}` })\n\nexport default ({ config }: any) => ({\n  ...config,\n  expo: {\n    name: 'musicfun-react-native-expo',\n    slug: 'musicfun-react-native-expo',\n    scheme: 'musicfun',\n    version: '1.0.0',\n    orientation: 'portrait',\n\n    icon: '',\n    splash: {\n      image: '',\n      resizeMode: 'contain',\n      backgroundColor: '#000000',\n    },\n\n    androidStatusBar: {\n      translucent: true,\n      backgroundColor: 'transparent',\n      barStyle: 'light-content',\n    },\n    iosStatusBar: {\n      translucent: true,\n      backgroundColor: 'transparent',\n      barStyle: 'light-content',\n    },\n    androidNavigationBar: {\n      backgroundColor: 'transparent',\n      barStyle: 'light-content',\n    },\n\n    ios: {\n      supportsTablet: false,\n      bundleIdentifier: 'com.your.bundle',\n    },\n    android: {\n      package: 'com.your.package',\n    },\n\n    extra: {\n      env: PROFILE,\n      API_BASE_URL: process.env.API_BASE_URL ?? '',\n      API_API_KEY: process.env.API_API_KEY ?? '',\n    },\n  },\n})\n"
  },
  {
    "path": "apps/react-native-expo/babel.config.js",
    "content": "module.exports = function (api) {\n  api.cache(true)\n  return {\n    presets: ['babel-preset-expo'],\n    plugins: [\n      'expo-router/babel',\n\n      [\n        'module-resolver',\n        {\n          root: ['.'],\n          alias: { '@': './' },\n          extensions: ['.tsx', '.ts', '.js', '.json'],\n        },\n      ],\n      'react-native-reanimated/plugin',\n    ],\n  }\n}\n"
  },
  {
    "path": "apps/react-native-expo/declarations.d.ts",
    "content": "declare module '*.svg' {\n  import type { FunctionComponent } from 'react'\n  import type { SvgProps } from 'react-native-svg'\n  const content: FunctionComponent<SvgProps>\n  export default content\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/components/LoginButton/LoginButton.tsx",
    "content": "import * as AuthSession from 'expo-auth-session'\nimport * as WebBrowser from 'expo-web-browser'\nimport { Fragment } from 'react'\n\nimport { REDIRECT_URI_EXPO } from '@/features/auth/model/config/oauth'\nimport { useAuthContext } from '@/features/auth/model/context/AuthContext'\nimport { API_ROOT, VERSION_ROOT } from '@/shared/api/api-root/api-root'\nimport { Button } from '@/shared/ui/Button/Button'\n\ntype LoginButtonPropsT = {}\n\nWebBrowser.maybeCompleteAuthSession()\n\nconst AUTH_URL = `${API_ROOT}${VERSION_ROOT}/auth/oauth-redirect?callbackUrl=${encodeURIComponent(REDIRECT_URI_EXPO)}`\n\nexport const LoginButton = ({}: LoginButtonPropsT) => {\n  const { login, isPending } = useAuthContext()\n\n  const onPressLogin = async () => {\n    const res = await WebBrowser.openAuthSessionAsync(AUTH_URL, REDIRECT_URI_EXPO)\n    if (res.type !== 'success' || !('url' in res)) return\n    const url = res.url\n    const code = new URL(url).searchParams.get('code')\n    if (!code) return\n\n    await login({ code, redirectUri: REDIRECT_URI_EXPO })\n  }\n\n  return (\n    <Fragment>\n      <Button disabled={isPending} isFull onPress={onPressLogin} title=\"Sign up with APIHUB\" />\n    </Fragment>\n  )\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/components/LogoutButton/LogoutButton.tsx",
    "content": "import { useRouter } from 'expo-router'\n\nimport { useAuthContext } from '@/features/auth/model/context/AuthContext'\nimport { Button } from '@/shared/ui/Button/Button'\n\nexport const LogoutButton = () => {\n  const { logout, isLogoutPending } = useAuthContext()\n  const router = useRouter()\n\n  const onLogout = async () => {\n    await logout()\n    router.replace('/(auth)/login')\n  }\n\n  return <Button disabled={isLogoutPending} title=\"Logout\" onPress={onLogout} />\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/api/auth-instanse/auth-instanse.ts",
    "content": "import {\n  RequestLoginT,\n  RequestLogoutT,\n  ResMeT,\n  ResponseLoginT,\n} from '@/features/auth/model/types/api.types'\nimport { API_PREFIX_ROOT } from '@/shared/api/api-root/api-root'\nimport { httpApiInterceptor } from '@/shared/api/api-root/api-root-instanse'\n\nexport default class apiAuthInstance {\n  private static api = httpApiInterceptor(API_PREFIX_ROOT.AUTH)\n\n  static me() {\n    return this.api.get<ResMeT>('me')\n  }\n  static login(arg: RequestLoginT) {\n    return this.api.post<ResponseLoginT>('/login', {\n      code: arg.code,\n      redirectUri: arg.redirectUri,\n      accessTokenTTL: arg.accessTokenTTL,\n      rememberMe: arg.rememberMe,\n    })\n  }\n  static logout(arg: RequestLogoutT) {\n    return this.api.post('/logout', {\n      refreshToken: arg.refreshToken,\n      rememberMe: arg.rememberMe,\n      accessTokenTTL: arg.accessTokenTTL,\n      redirectUri: arg.redirectUri,\n    })\n  }\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/api/hooks/use-login-mutatuion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport apiAuthInstance from '@/features/auth/model/api/auth-instanse/auth-instanse'\nimport { tokenStorage } from '@/shared/storage/tokenStorage'\n\nexport const useLoginMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: async ({ code, redirectUri }: { code: string; redirectUri: string }) => {\n      const response = await apiAuthInstance.login({\n        code,\n        redirectUri: redirectUri,\n        accessTokenTTL: '10m',\n        rememberMe: true,\n      })\n\n      return response.data\n    },\n    onSuccess: async (data) => {\n      console.log('data', data)\n      await tokenStorage.set({ accessToken: data.accessToken, refreshToken: data.refreshToken })\n\n      await queryClient.invalidateQueries({ queryKey: ['auth', 'me'] })\n      //   await queryClient.refetchQueries({ queryKey: ['auth','me'], type: 'all' })\n    },\n  })\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/api/hooks/use-logout-mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport apiAuthInstance from '@/features/auth/model/api/auth-instanse/auth-instanse'\nimport { REDIRECT_URI_EXPO } from '@/features/auth/model/config/oauth'\nimport { tokenStorage } from '@/shared/storage/tokenStorage'\n\nexport const useLogoutMutation = () => {\n  const queryClient = useQueryClient()\n  const mutation = useMutation({\n    mutationFn: async () => {\n      const refreshToken = await tokenStorage.getRefresh()\n      if (!refreshToken) return null\n\n      const response = await apiAuthInstance.logout({\n        refreshToken,\n        rememberMe: true,\n        accessTokenTTL: '1d',\n        redirectUri: REDIRECT_URI_EXPO,\n      })\n\n      return response.data\n    },\n    onSuccess: async () => {\n      await queryClient.resetQueries({ queryKey: ['auth', 'me'] })\n    },\n  })\n\n  return mutation\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/api/hooks/use-me.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport apiAuthInstance from '@/features/auth/model/api/auth-instanse/auth-instanse'\n\nexport const useMeQuery = () => {\n  return useQuery({\n    queryKey: ['auth', 'me'],\n    queryFn: async () => {\n      const res = await apiAuthInstance.me()\n      return res.data\n    },\n  })\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/config/oauth.ts",
    "content": "import * as AuthSession from 'expo-auth-session'\n\nconst base = AuthSession.makeRedirectUri({ scheme: 'musicfun' })\nexport const REDIRECT_URI_EXPO = `${base}/oauth/callback`\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/context/AuthContext.tsx",
    "content": "import { useQueryClient } from '@tanstack/react-query'\nimport { createContext, ReactNode, useContext, useEffect, useState } from 'react'\n\nimport { useLoginMutation } from '@/features/auth/model/api/hooks/use-login-mutatuion'\nimport { useLogoutMutation } from '@/features/auth/model/api/hooks/use-logout-mutation'\nimport { tokenStorage } from '@/shared/storage/tokenStorage'\n\ntype LoginParams = { code: string; redirectUri: string }\ntype AuthContextT = {\n  accessToken: string | null\n  isAuth: boolean\n  isPending: boolean\n  isLogoutPending: boolean\n  login: (params: LoginParams) => Promise<void>\n  logout: () => Promise<void>\n}\n\nconst AuthContext = createContext<AuthContextT | undefined>(undefined)\n\nexport const AuthContextProvider = ({ children }: { children: ReactNode }) => {\n  const [accessToken, setAccessToken] = useState<string | null>(null)\n  const [booted, setBooted] = useState(false)\n  const qc = useQueryClient()\n  const { mutateAsync: loginMutation, isPending } = useLoginMutation()\n  const { mutateAsync: logoutMutation, isPending: isLogoutPending } = useLogoutMutation()\n\n  useEffect(() => {\n    const init = async () => {\n      const token = await tokenStorage.getAccess()\n      setAccessToken(token)\n      setBooted(true)\n    }\n    init().then((r) => r)\n  }, [])\n\n  const login = async ({ code, redirectUri }: LoginParams) => {\n    const data = await loginMutation({ code, redirectUri })\n    await tokenStorage.set({\n      accessToken: data.accessToken,\n      refreshToken: data.refreshToken,\n    })\n    setAccessToken(data.accessToken)\n  }\n\n  const logout = async () => {\n    await logoutMutation()\n    await tokenStorage.clear()\n    setAccessToken(null)\n    await qc.resetQueries()\n  }\n\n  if (!booted) return null\n  return (\n    <AuthContext.Provider\n      value={{\n        accessToken,\n        isAuth: !!accessToken,\n        login,\n        logout,\n        isPending,\n        isLogoutPending,\n      }}>\n      {children}\n    </AuthContext.Provider>\n  )\n}\n\nexport const useAuthContext = () => {\n  const ctx = useContext(AuthContext)\n  if (!ctx) console.warn('контекст AuthContext не найден')\n  return ctx\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/types/api.types.ts",
    "content": "export type ResMeT = {\n  userId: string\n  login: string\n}\n\nexport type RequestLoginT = {\n  code: string\n  redirectUri: string\n  accessTokenTTL: string\n  rememberMe: boolean\n}\n\nexport type RequestLogoutT = {\n  refreshToken: string\n  rememberMe: boolean\n  accessTokenTTL: string\n  redirectUri: string\n}\n\nexport type ResponseLoginT = {\n  accessToken: string\n  refreshToken: string\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/utils/expoUrlToHttpCallback.ts",
    "content": "export function expoUrlToHttpCallback(expoUrl: string): string {\n  try {\n    // убираем префикс exp://\n    const url = expoUrl.replace(/^exp:\\/\\//, '')\n    return `http://${url}/oauth/callback`\n  } catch {\n    throw new Error('Invalid expo URL')\n  }\n}\n"
  },
  {
    "path": "apps/react-native-expo/features/auth/model/utils/getOauthRedirectUrl.ts",
    "content": "import { API_ROOT } from '@/shared/api/api-root/api-root'\n\nexport const getOauthRedirectUrl = (redirectUrl: string) =>\n  `${API_ROOT}/auth/oauth-redirect?callbackUrl=${encodeURIComponent(redirectUrl)}`\n"
  },
  {
    "path": "apps/react-native-expo/features/playlists/model/api/playlist-instance/playlist-instance.ts",
    "content": "import { ResponseLoginT } from '@/features/auth/model/types/api.types'\nimport { API_PREFIX_ROOT } from '@/shared/api/api-root/api-root'\nimport { httpApiInterceptor } from '@/shared/api/api-root/api-root-instanse'\n\nexport default class AuthPlaylistInstance {\n  private static api = httpApiInterceptor(API_PREFIX_ROOT.PLAYLISTS)\n\n  static getPlaylist() {\n    return this.api.get<ResponseLoginT>('')\n  }\n}\n"
  },
  {
    "path": "apps/react-native-expo/index.ts",
    "content": "import { registerRootComponent } from 'expo'\n\nimport App from './App'\n\n// registerRootComponent calls AppRegistry.registerComponent('main', () => App);\n// It also ensures that whether you load the app in Expo Go or in a native build,\n// the environment is set up appropriately\nregisterRootComponent(App)\n"
  },
  {
    "path": "apps/react-native-expo/metro.config.js",
    "content": "const { getDefaultConfig } = require('expo/metro-config')\nconst config = getDefaultConfig(__dirname)\nconst { assetExts, sourceExts } = config.resolver\nconfig.resolver.assetExts = assetExts.filter((ext) => ext !== 'svg')\nconfig.resolver.sourceExts = [...sourceExts, 'svg']\nconfig.transformer.babelTransformerPath = require.resolve('react-native-svg-transformer')\n\nconfig.transformer.unstable_allowRequireContext = true\nmodule.exports = config\n"
  },
  {
    "path": "apps/react-native-expo/package.json",
    "content": "{\n  \"name\": \"musicfun-react-native-expo\",\n  \"license\": \"0BSD\",\n  \"version\": \"1.0.0\",\n  \"main\": \"expo-router/entry\",\n  \"scripts\": {\n    \"start_clear\": \"pnpm run start:dev -- --clear\",\n    \"start\": \"expo start\",\n    \"start:dev\": \"cross-env APP_ENV=development expo start\",\n    \"start:prod\": \"cross-env APP_ENV=production expo start\",\n    \"android\": \"expo start --android\",\n    \"ios\": \"expo start --ios\",\n    \"web\": \"expo start --web\"\n  },\n  \"dependencies\": {\n    \"@expo/metro-runtime\": \"~6.1.2\",\n    \"@react-buoy/core\": \"^0.1.12\",\n    \"@react-buoy/react-query\": \"^0.1.12\",\n    \"@react-buoy/shared-ui\": \"^0.1.12\",\n    \"@react-native-async-storage/async-storage\": \"^2.2.0\",\n    \"@react-native-community/netinfo\": \"^11.4.1\",\n    \"@tanstack/query-async-storage-persister\": \"^5.90.2\",\n    \"@tanstack/react-query\": \"^5.90.2\",\n    \"@tanstack/react-query-persist-client\": \"^5.90.2\",\n    \"axios\": \"^1.12.2\",\n    \"babel-plugin-module-resolver\": \"^5.0.2\",\n    \"dotenv\": \"^17.2.3\",\n    \"expo\": \"~54.0.12\",\n    \"expo-auth-session\": \"^7.0.8\",\n    \"expo-constants\": \"~18.0.9\",\n    \"expo-font\": \"^14.0.8\",\n    \"expo-linking\": \"~8.0.8\",\n    \"expo-router\": \"~6.0.10\",\n    \"expo-secure-store\": \"^15.0.7\",\n    \"expo-status-bar\": \"~3.0.8\",\n    \"expo-web-browser\": \"^15.0.8\",\n    \"react\": \"19.1.0\",\n    \"react-native\": \"0.81.4\",\n    \"react-native-gesture-handler\": \"^2.28.0\",\n    \"react-native-reanimated\": \"~4.1.1\",\n    \"react-native-safe-area-context\": \"~5.6.1\",\n    \"react-native-screens\": \"~4.16.0\",\n    \"react-native-svg\": \"^15.12.1\",\n    \"react-native-svg-transformer\": \"^1.5.1\",\n    \"react-native-worklets\": \"0.5.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"~19.1.0\",\n    \"cross-env\": \"^10.1.0\",\n    \"typescript\": \"~5.9.2\"\n  },\n  \"pnpm\": {\n    \"overrides\": {\n      \"@babel/core\": \"7.28.4\",\n      \"@babel/parser\": \"7.28.4\",\n      \"@babel/types\": \"7.28.4\",\n      \"babel-preset-expo\": \"54.0.3\"\n    }\n  }\n}\n"
  },
  {
    "path": "apps/react-native-expo/pnpm-lock.yaml.2457664388",
    "content": "lockfileVersion: '6.0'\n\nsettings:\n  autoInstallPeers: true\n  excludeLinksFromLockfile: false\n\noverrides:\n  '@babel/core': 7.28.4\n  '@babel/parser': 7.28.4\n  '@babel/types': 7.28.4\n  babel-preset-expo: 54.0.3\n\ndependencies:\n  '@expo/metro-runtime':\n    specifier: ~6.1.2\n    version: 6.1.2(expo@54.0.10)(react-dom@19.1.1)(react-native@0.81.4)(react@19.1.0)\n  '@react-buoy/core':\n    specifier: ^0.1.12\n    version: 0.1.12(react-native@0.81.4)(react@19.1.0)\n  '@react-buoy/react-query':\n    specifier: ^0.1.12\n    version: 0.1.12(@tanstack/react-query@5.90.2)(react-native@0.81.4)(react@19.1.0)\n  '@react-buoy/shared-ui':\n    specifier: ^0.1.12\n    version: 0.1.12(react-native@0.81.4)(react@19.1.0)\n  '@react-native-async-storage/async-storage':\n    specifier: ^2.2.0\n    version: 2.2.0(react-native@0.81.4)\n  '@react-native-community/netinfo':\n    specifier: ^11.4.1\n    version: 11.4.1(react-native@0.81.4)\n  '@tanstack/query-async-storage-persister':\n    specifier: ^5.90.2\n    version: 5.90.2\n  '@tanstack/react-query':\n    specifier: ^5.90.2\n    version: 5.90.2(react@19.1.0)\n  '@tanstack/react-query-persist-client':\n    specifier: ^5.90.2\n    version: 5.90.2(@tanstack/react-query@5.90.2)(react@19.1.0)\n  axios:\n    specifier: ^1.12.2\n    version: 1.12.2\n  babel-plugin-module-resolver:\n    specifier: ^5.0.2\n    version: 5.0.2\n  dotenv:\n    specifier: ^17.2.3\n    version: 17.2.3\n  expo:\n    specifier: ~54.0.10\n    version: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n  expo-constants:\n    specifier: ~18.0.9\n    version: 18.0.9(expo@54.0.10)(react-native@0.81.4)\n  expo-font:\n    specifier: ^14.0.8\n    version: 14.0.8(expo@54.0.10)(react-native@0.81.4)(react@19.1.0)\n  expo-linking:\n    specifier: ~8.0.8\n    version: 8.0.8(expo@54.0.10)(react-native@0.81.4)(react@19.1.0)\n  expo-router:\n    specifier: ~6.0.8\n    version: 6.0.8(@expo/metro-runtime@6.1.2)(@types/react@19.1.13)(expo-constants@18.0.9)(expo-linking@8.0.8)(expo@54.0.10)(react-dom@19.1.1)(react-native-gesture-handler@2.28.0)(react-native-reanimated@4.1.2)(react-native-safe-area-context@5.6.1)(react-native-screens@4.16.0)(react-native@0.81.4)(react@19.1.0)\n  expo-status-bar:\n    specifier: ~3.0.8\n    version: 3.0.8(react-native@0.81.4)(react@19.1.0)\n  react:\n    specifier: 19.1.0\n    version: 19.1.0\n  react-native:\n    specifier: 0.81.4\n    version: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n  react-native-gesture-handler:\n    specifier: ^2.28.0\n    version: 2.28.0(react-native@0.81.4)(react@19.1.0)\n  react-native-reanimated:\n    specifier: ~4.1.1\n    version: 4.1.2(@babel/core@7.28.4)(react-native-worklets@0.5.1)(react-native@0.81.4)(react@19.1.0)\n  react-native-safe-area-context:\n    specifier: ~5.6.1\n    version: 5.6.1(react-native@0.81.4)(react@19.1.0)\n  react-native-screens:\n    specifier: ~4.16.0\n    version: 4.16.0(react-native@0.81.4)(react@19.1.0)\n  react-native-svg:\n    specifier: ^15.12.1\n    version: 15.12.1(react-native@0.81.4)(react@19.1.0)\n  react-native-svg-transformer:\n    specifier: ^1.5.1\n    version: 1.5.1(react-native-svg@15.12.1)(react-native@0.81.4)(typescript@5.9.2)\n  react-native-worklets:\n    specifier: 0.5.1\n    version: 0.5.1(@babel/core@7.28.4)(react-native@0.81.4)(react@19.1.0)\n\ndevDependencies:\n  '@types/react':\n    specifier: ~19.1.0\n    version: 19.1.13\n  cross-env:\n    specifier: ^10.1.0\n    version: 10.1.0\n  typescript:\n    specifier: ~5.9.2\n    version: 5.9.2\n\npackages:\n\n  /@0no-co/graphql.web@1.2.0:\n    resolution: {integrity: sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==}\n    peerDependencies:\n      graphql: ^14.0.0 || ^15.0.0 || ^16.0.0\n    peerDependenciesMeta:\n      graphql:\n        optional: true\n    dev: false\n\n  /@babel/code-frame@7.10.4:\n    resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==}\n    dependencies:\n      '@babel/highlight': 7.25.9\n    dev: false\n\n  /@babel/code-frame@7.27.1:\n    resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/helper-validator-identifier': 7.27.1\n      js-tokens: 4.0.0\n      picocolors: 1.1.1\n    dev: false\n\n  /@babel/compat-data@7.28.4:\n    resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==}\n    engines: {node: '>=6.9.0'}\n    dev: false\n\n  /@babel/core@7.28.4:\n    resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/code-frame': 7.27.1\n      '@babel/generator': 7.28.3\n      '@babel/helper-compilation-targets': 7.27.2\n      '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4)\n      '@babel/helpers': 7.28.4\n      '@babel/parser': 7.28.4\n      '@babel/template': 7.27.2\n      '@babel/traverse': 7.28.4\n      '@babel/types': 7.28.4\n      '@jridgewell/remapping': 2.3.5\n      convert-source-map: 2.0.0\n      debug: 4.4.3\n      gensync: 1.0.0-beta.2\n      json5: 2.2.3\n      semver: 6.3.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/generator@7.28.3:\n    resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/parser': 7.28.4\n      '@babel/types': 7.28.4\n      '@jridgewell/gen-mapping': 0.3.13\n      '@jridgewell/trace-mapping': 0.3.31\n      jsesc: 3.1.0\n    dev: false\n\n  /@babel/helper-annotate-as-pure@7.27.3:\n    resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/types': 7.28.4\n    dev: false\n\n  /@babel/helper-compilation-targets@7.27.2:\n    resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/compat-data': 7.28.4\n      '@babel/helper-validator-option': 7.27.1\n      browserslist: 4.26.2\n      lru-cache: 5.1.1\n      semver: 6.3.1\n    dev: false\n\n  /@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-annotate-as-pure': 7.27.3\n      '@babel/helper-member-expression-to-functions': 7.27.1\n      '@babel/helper-optimise-call-expression': 7.27.1\n      '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4)\n      '@babel/helper-skip-transparent-expression-wrappers': 7.27.1\n      '@babel/traverse': 7.28.4\n      semver: 6.3.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-annotate-as-pure': 7.27.3\n      regexpu-core: 6.3.1\n      semver: 6.3.1\n    dev: false\n\n  /@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.4):\n    resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-compilation-targets': 7.27.2\n      '@babel/helper-plugin-utils': 7.27.1\n      debug: 4.4.3\n      lodash.debounce: 4.0.8\n      resolve: 1.22.10\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helper-globals@7.28.0:\n    resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}\n    engines: {node: '>=6.9.0'}\n    dev: false\n\n  /@babel/helper-member-expression-to-functions@7.27.1:\n    resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/traverse': 7.28.4\n      '@babel/types': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helper-module-imports@7.27.1:\n    resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/traverse': 7.28.4\n      '@babel/types': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-module-imports': 7.27.1\n      '@babel/helper-validator-identifier': 7.27.1\n      '@babel/traverse': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helper-optimise-call-expression@7.27.1:\n    resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/types': 7.28.4\n    dev: false\n\n  /@babel/helper-plugin-utils@7.27.1:\n    resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}\n    engines: {node: '>=6.9.0'}\n    dev: false\n\n  /@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-annotate-as-pure': 7.27.3\n      '@babel/helper-wrap-function': 7.28.3\n      '@babel/traverse': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-member-expression-to-functions': 7.27.1\n      '@babel/helper-optimise-call-expression': 7.27.1\n      '@babel/traverse': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helper-skip-transparent-expression-wrappers@7.27.1:\n    resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/traverse': 7.28.4\n      '@babel/types': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helper-string-parser@7.27.1:\n    resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}\n    engines: {node: '>=6.9.0'}\n    dev: false\n\n  /@babel/helper-validator-identifier@7.27.1:\n    resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}\n    engines: {node: '>=6.9.0'}\n    dev: false\n\n  /@babel/helper-validator-option@7.27.1:\n    resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}\n    engines: {node: '>=6.9.0'}\n    dev: false\n\n  /@babel/helper-wrap-function@7.28.3:\n    resolution: {integrity: sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/template': 7.27.2\n      '@babel/traverse': 7.28.4\n      '@babel/types': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/helpers@7.28.4:\n    resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/template': 7.27.2\n      '@babel/types': 7.28.4\n    dev: false\n\n  /@babel/highlight@7.25.9:\n    resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/helper-validator-identifier': 7.27.1\n      chalk: 2.4.2\n      js-tokens: 4.0.0\n      picocolors: 1.1.1\n    dev: false\n\n  /@babel/parser@7.28.4:\n    resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==}\n    engines: {node: '>=6.0.0'}\n    hasBin: true\n    dependencies:\n      '@babel/types': 7.28.4\n    dev: false\n\n  /@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4):\n    resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4):\n    resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-export-default-from@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4):\n    resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4):\n    resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4)\n      '@babel/traverse': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-module-imports': 7.27.1\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-block-scoping@7.28.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-annotate-as-pure': 7.27.3\n      '@babel/helper-compilation-targets': 7.27.2\n      '@babel/helper-globals': 7.28.0\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4)\n      '@babel/traverse': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/template': 7.27.2\n    dev: false\n\n  /@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/traverse': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.4)\n    dev: false\n\n  /@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-skip-transparent-expression-wrappers': 7.27.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-compilation-targets': 7.27.2\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/traverse': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-compilation-targets': 7.27.2\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4)\n      '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4)\n      '@babel/traverse': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-skip-transparent-expression-wrappers': 7.27.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.4):\n    resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-annotate-as-pure': 7.27.3\n      '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-annotate-as-pure': 7.27.3\n      '@babel/helper-module-imports': 7.27.1\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4)\n      '@babel/types': 7.28.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-annotate-as-pure': 7.27.3\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-runtime@7.28.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-module-imports': 7.27.1\n      '@babel/helper-plugin-utils': 7.27.1\n      babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.4)\n      babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.4)\n      babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.4)\n      semver: 6.3.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-skip-transparent-expression-wrappers': 7.27.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-annotate-as-pure': 7.27.3\n      '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-skip-transparent-expression-wrappers': 7.27.1\n      '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4)\n      '@babel/helper-plugin-utils': 7.27.1\n    dev: false\n\n  /@babel/preset-react@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-validator-option': 7.27.1\n      '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.4)\n      '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/preset-typescript@7.27.1(@babel/core@7.28.4):\n    resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==}\n    engines: {node: '>=6.9.0'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-plugin-utils': 7.27.1\n      '@babel/helper-validator-option': 7.27.1\n      '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/runtime@7.28.4:\n    resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}\n    engines: {node: '>=6.9.0'}\n    dev: false\n\n  /@babel/template@7.27.2:\n    resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/code-frame': 7.27.1\n      '@babel/parser': 7.28.4\n      '@babel/types': 7.28.4\n    dev: false\n\n  /@babel/traverse@7.28.4:\n    resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/code-frame': 7.27.1\n      '@babel/generator': 7.28.3\n      '@babel/helper-globals': 7.28.0\n      '@babel/parser': 7.28.4\n      '@babel/template': 7.27.2\n      '@babel/types': 7.28.4\n      debug: 4.4.3\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@babel/types@7.28.4:\n    resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}\n    engines: {node: '>=6.9.0'}\n    dependencies:\n      '@babel/helper-string-parser': 7.27.1\n      '@babel/helper-validator-identifier': 7.27.1\n    dev: false\n\n  /@egjs/hammerjs@2.0.17:\n    resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==}\n    engines: {node: '>=0.8.0'}\n    dependencies:\n      '@types/hammerjs': 2.0.46\n    dev: false\n\n  /@epic-web/invariant@1.0.0:\n    resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}\n    dev: true\n\n  /@expo/cli@54.0.8(expo-router@6.0.8)(expo@54.0.10)(react-native@0.81.4):\n    resolution: {integrity: sha512-bRJXvtjgxpyElmJuKLotWyIW5j9a2K3rGUjd2A8LRcFimrZp0wwuKPQjlUK0sFNbU7zHWfxubNq/B+UkUNkCxw==}\n    hasBin: true\n    peerDependencies:\n      expo: '*'\n      expo-router: '*'\n      react-native: '*'\n    peerDependenciesMeta:\n      expo-router:\n        optional: true\n      react-native:\n        optional: true\n    dependencies:\n      '@0no-co/graphql.web': 1.2.0\n      '@expo/code-signing-certificates': 0.0.5\n      '@expo/config': 12.0.9\n      '@expo/config-plugins': 54.0.1\n      '@expo/devcert': 1.2.0\n      '@expo/env': 2.0.7\n      '@expo/image-utils': 0.8.7\n      '@expo/json-file': 10.0.7\n      '@expo/mcp-tunnel': 0.0.7\n      '@expo/metro': 54.0.0\n      '@expo/metro-config': 54.0.5(expo@54.0.10)\n      '@expo/osascript': 2.3.7\n      '@expo/package-manager': 1.9.8\n      '@expo/plist': 0.4.7\n      '@expo/prebuild-config': 54.0.3(expo@54.0.10)\n      '@expo/schema-utils': 0.1.7\n      '@expo/server': 0.7.5\n      '@expo/spawn-async': 1.7.2\n      '@expo/ws-tunnel': 1.0.6\n      '@expo/xcpretty': 4.3.2\n      '@react-native/dev-middleware': 0.81.4\n      '@urql/core': 5.2.0\n      '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0)\n      accepts: 1.3.8\n      arg: 5.0.2\n      better-opn: 3.0.2\n      bplist-creator: 0.1.0\n      bplist-parser: 0.3.2\n      chalk: 4.1.2\n      ci-info: 3.9.0\n      compression: 1.8.1\n      connect: 3.7.0\n      debug: 4.4.3\n      env-editor: 0.4.2\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      expo-router: 6.0.8(@expo/metro-runtime@6.1.2)(@types/react@19.1.13)(expo-constants@18.0.9)(expo-linking@8.0.8)(expo@54.0.10)(react-dom@19.1.1)(react-native-gesture-handler@2.28.0)(react-native-reanimated@4.1.2)(react-native-safe-area-context@5.6.1)(react-native-screens@4.16.0)(react-native@0.81.4)(react@19.1.0)\n      freeport-async: 2.0.0\n      getenv: 2.0.0\n      glob: 10.4.5\n      lan-network: 0.1.7\n      minimatch: 9.0.5\n      node-forge: 1.3.1\n      npm-package-arg: 11.0.3\n      ora: 3.4.0\n      picomatch: 3.0.1\n      pretty-bytes: 5.6.0\n      pretty-format: 29.7.0\n      progress: 2.0.3\n      prompts: 2.4.2\n      qrcode-terminal: 0.11.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      require-from-string: 2.0.2\n      requireg: 0.2.2\n      resolve: 1.22.10\n      resolve-from: 5.0.0\n      resolve.exports: 2.0.3\n      semver: 7.7.2\n      send: 0.19.1\n      slugify: 1.6.6\n      source-map-support: 0.5.21\n      stacktrace-parser: 0.1.11\n      structured-headers: 0.4.1\n      tar: 7.4.3\n      terminal-link: 2.1.1\n      undici: 6.21.3\n      wrap-ansi: 7.0.0\n      ws: 8.18.3\n    transitivePeerDependencies:\n      - '@modelcontextprotocol/sdk'\n      - bufferutil\n      - graphql\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /@expo/code-signing-certificates@0.0.5:\n    resolution: {integrity: sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==}\n    dependencies:\n      node-forge: 1.3.1\n      nullthrows: 1.1.1\n    dev: false\n\n  /@expo/config-plugins@54.0.1:\n    resolution: {integrity: sha512-NyBChhiWFL6VqSgU+LzK4R1vC397tEG2XFewVt4oMr4Pnalq/mJxBANQrR+dyV1RHhSyhy06RNiJIkQyngVWeg==}\n    dependencies:\n      '@expo/config-types': 54.0.8\n      '@expo/json-file': 10.0.7\n      '@expo/plist': 0.4.7\n      '@expo/sdk-runtime-versions': 1.0.0\n      chalk: 4.1.2\n      debug: 4.4.3\n      getenv: 2.0.0\n      glob: 10.4.5\n      resolve-from: 5.0.0\n      semver: 7.7.2\n      slash: 3.0.0\n      slugify: 1.6.6\n      xcode: 3.0.1\n      xml2js: 0.6.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@expo/config-types@54.0.8:\n    resolution: {integrity: sha512-lyIn/x/Yz0SgHL7IGWtgTLg6TJWC9vL7489++0hzCHZ4iGjVcfZmPTUfiragZ3HycFFj899qN0jlhl49IHa94A==}\n    dev: false\n\n  /@expo/config@12.0.9:\n    resolution: {integrity: sha512-HiDVVaXYKY57+L1MxSF3TaYjX6zZlGBnuWnOKZG+7mtsLD+aNTtW4bZM0pZqZfoRumyOU0SfTCwT10BWtUUiJQ==}\n    dependencies:\n      '@babel/code-frame': 7.10.4\n      '@expo/config-plugins': 54.0.1\n      '@expo/config-types': 54.0.8\n      '@expo/json-file': 10.0.7\n      deepmerge: 4.3.1\n      getenv: 2.0.0\n      glob: 10.4.5\n      require-from-string: 2.0.2\n      resolve-from: 5.0.0\n      resolve-workspace-root: 2.0.0\n      semver: 7.7.2\n      slugify: 1.6.6\n      sucrase: 3.35.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@expo/devcert@1.2.0:\n    resolution: {integrity: sha512-Uilcv3xGELD5t/b0eM4cxBFEKQRIivB3v7i+VhWLV/gL98aw810unLKKJbGAxAIhY6Ipyz8ChWibFsKFXYwstA==}\n    dependencies:\n      '@expo/sudo-prompt': 9.3.2\n      debug: 3.2.7\n      glob: 10.4.5\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@expo/devtools@0.1.7(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-dfIa9qMyXN+0RfU6SN4rKeXZyzKWsnz6xBSDccjL4IRiE+fQ0t84zg0yxgN4t/WK2JU5v6v4fby7W7Crv9gJvA==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    peerDependenciesMeta:\n      react:\n        optional: true\n      react-native:\n        optional: true\n    dependencies:\n      chalk: 4.1.2\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /@expo/env@2.0.7:\n    resolution: {integrity: sha512-BNETbLEohk3HQ2LxwwezpG8pq+h7Fs7/vAMP3eAtFT1BCpprLYoBBFZH7gW4aqGfqOcVP4Lc91j014verrYNGg==}\n    dependencies:\n      chalk: 4.1.2\n      debug: 4.4.3\n      dotenv: 16.4.7\n      dotenv-expand: 11.0.7\n      getenv: 2.0.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@expo/fingerprint@0.15.1:\n    resolution: {integrity: sha512-U1S9DwiapCHQjHdHDDyO/oXsl/1oEHSHZRRkWDDrHgXRUDiAVIySw9Unvvcr118Ee6/x4NmKSZY1X0VagrqmFg==}\n    hasBin: true\n    dependencies:\n      '@expo/spawn-async': 1.7.2\n      arg: 5.0.2\n      chalk: 4.1.2\n      debug: 4.4.3\n      getenv: 2.0.0\n      glob: 10.4.5\n      ignore: 5.3.2\n      minimatch: 9.0.5\n      p-limit: 3.1.0\n      resolve-from: 5.0.0\n      semver: 7.7.2\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@expo/image-utils@0.8.7:\n    resolution: {integrity: sha512-SXOww4Wq3RVXLyOaXiCCuQFguCDh8mmaHBv54h/R29wGl4jRY8GEyQEx8SypV/iHt1FbzsU/X3Qbcd9afm2W2w==}\n    dependencies:\n      '@expo/spawn-async': 1.7.2\n      chalk: 4.1.2\n      getenv: 2.0.0\n      jimp-compact: 0.16.1\n      parse-png: 2.1.0\n      resolve-from: 5.0.0\n      resolve-global: 1.0.0\n      semver: 7.7.2\n      temp-dir: 2.0.0\n      unique-string: 2.0.0\n    dev: false\n\n  /@expo/json-file@10.0.7:\n    resolution: {integrity: sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw==}\n    dependencies:\n      '@babel/code-frame': 7.10.4\n      json5: 2.2.3\n    dev: false\n\n  /@expo/mcp-tunnel@0.0.7:\n    resolution: {integrity: sha512-ht8Q1nKtiHobZqkUqt/7awwjW2D59ardP6XDVmGceGjQtoZELVaJDHyMIX+aVG9SZ9aj8+uGlhQYeBi57SZPMA==}\n    peerDependencies:\n      '@modelcontextprotocol/sdk': ^1.13.2\n    peerDependenciesMeta:\n      '@modelcontextprotocol/sdk':\n        optional: true\n    dependencies:\n      ws: 8.18.3\n      zod: 3.25.76\n      zod-to-json-schema: 3.24.6(zod@3.25.76)\n    transitivePeerDependencies:\n      - bufferutil\n      - utf-8-validate\n    dev: false\n\n  /@expo/metro-config@54.0.5(expo@54.0.10):\n    resolution: {integrity: sha512-Y+oYtLg8b3L4dHFImfu8+yqO+KOcBpLLjxN7wGbs7miP/BjntBQ6tKbPxyKxHz5UUa1s+buBzZlZhsFo9uqKMg==}\n    peerDependencies:\n      expo: '*'\n    peerDependenciesMeta:\n      expo:\n        optional: true\n    dependencies:\n      '@babel/code-frame': 7.27.1\n      '@babel/core': 7.28.4\n      '@babel/generator': 7.28.3\n      '@expo/config': 12.0.9\n      '@expo/env': 2.0.7\n      '@expo/json-file': 10.0.7\n      '@expo/metro': 54.0.0\n      '@expo/spawn-async': 1.7.2\n      browserslist: 4.26.2\n      chalk: 4.1.2\n      debug: 4.4.3\n      dotenv: 16.4.7\n      dotenv-expand: 11.0.7\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      getenv: 2.0.0\n      glob: 10.4.5\n      hermes-parser: 0.29.1\n      jsc-safe-url: 0.2.4\n      lightningcss: 1.30.1\n      minimatch: 9.0.5\n      postcss: 8.4.49\n      resolve-from: 5.0.0\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /@expo/metro-runtime@6.1.2(expo@54.0.10)(react-dom@19.1.1)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==}\n    peerDependencies:\n      expo: '*'\n      react: '*'\n      react-dom: '*'\n      react-native: '*'\n    peerDependenciesMeta:\n      react-dom:\n        optional: true\n    dependencies:\n      anser: 1.4.10\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      pretty-format: 29.7.0\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      stacktrace-parser: 0.1.11\n      whatwg-fetch: 3.6.20\n    dev: false\n\n  /@expo/metro@54.0.0:\n    resolution: {integrity: sha512-x2HlliepLJVLSe0Fl/LuPT83Mn2EXpPlb1ngVtcawlz4IfbkYJo16/Zfsfrn1t9d8LpN5dD44Dc55Q1/fO05Nw==}\n    dependencies:\n      metro: 0.83.1\n      metro-babel-transformer: 0.83.1\n      metro-cache: 0.83.1\n      metro-cache-key: 0.83.1\n      metro-config: 0.83.1\n      metro-core: 0.83.1\n      metro-file-map: 0.83.1\n      metro-resolver: 0.83.1\n      metro-runtime: 0.83.1\n      metro-source-map: 0.83.1\n      metro-transform-plugins: 0.83.1\n      metro-transform-worker: 0.83.1\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /@expo/osascript@2.3.7:\n    resolution: {integrity: sha512-IClSOXxR0YUFxIriUJVqyYki7lLMIHrrzOaP01yxAL1G8pj2DWV5eW1y5jSzIcIfSCNhtGsshGd1tU/AYup5iQ==}\n    engines: {node: '>=12'}\n    dependencies:\n      '@expo/spawn-async': 1.7.2\n      exec-async: 2.2.0\n    dev: false\n\n  /@expo/package-manager@1.9.8:\n    resolution: {integrity: sha512-4/I6OWquKXYnzo38pkISHCOCOXxfeEmu4uDoERq1Ei/9Ur/s9y3kLbAamEkitUkDC7gHk1INxRWEfFNzGbmOrA==}\n    dependencies:\n      '@expo/json-file': 10.0.7\n      '@expo/spawn-async': 1.7.2\n      chalk: 4.1.2\n      npm-package-arg: 11.0.3\n      ora: 3.4.0\n      resolve-workspace-root: 2.0.0\n    dev: false\n\n  /@expo/plist@0.4.7:\n    resolution: {integrity: sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA==}\n    dependencies:\n      '@xmldom/xmldom': 0.8.11\n      base64-js: 1.5.1\n      xmlbuilder: 15.1.1\n    dev: false\n\n  /@expo/prebuild-config@54.0.3(expo@54.0.10):\n    resolution: {integrity: sha512-okf6Umaz1VniKmm+pA37QHBzB9XlRHvO1Qh3VbUezy07LTkz87kXUW7uLMmrA319WLavWSVORTXeR0jBRihObA==}\n    peerDependencies:\n      expo: '*'\n    dependencies:\n      '@expo/config': 12.0.9\n      '@expo/config-plugins': 54.0.1\n      '@expo/config-types': 54.0.8\n      '@expo/image-utils': 0.8.7\n      '@expo/json-file': 10.0.7\n      '@react-native/normalize-colors': 0.81.4\n      debug: 4.4.3\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      resolve-from: 5.0.0\n      semver: 7.7.2\n      xml2js: 0.6.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@expo/schema-utils@0.1.7:\n    resolution: {integrity: sha512-jWHoSuwRb5ZczjahrychMJ3GWZu54jK9ulNdh1d4OzAEq672K9E5yOlnlBsfIHWHGzUAT+0CL7Yt1INiXTz68g==}\n    dev: false\n\n  /@expo/sdk-runtime-versions@1.0.0:\n    resolution: {integrity: sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==}\n    dev: false\n\n  /@expo/server@0.7.5:\n    resolution: {integrity: sha512-aNVcerBSJEcUspvXRWChEgFhix1gTNIcgFDevaU/A1+TkfbejNIjGX4rfLEpfyRzzdLIRuOkBNjD+uTYMzohyg==}\n    engines: {node: '>=20.16.0'}\n    dependencies:\n      abort-controller: 3.0.0\n      debug: 4.4.3\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@expo/spawn-async@1.7.2:\n    resolution: {integrity: sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==}\n    engines: {node: '>=12'}\n    dependencies:\n      cross-spawn: 7.0.6\n    dev: false\n\n  /@expo/sudo-prompt@9.3.2:\n    resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==}\n    dev: false\n\n  /@expo/vector-icons@15.0.2(expo-font@14.0.8)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-IiBjg7ZikueuHNf40wSGCf0zS73a3guJLdZzKnDUxsauB8VWPLMeWnRIupc+7cFhLUkqyvyo0jLNlcxG5xPOuQ==}\n    peerDependencies:\n      expo-font: '>=14.0.4'\n      react: '*'\n      react-native: '*'\n    dependencies:\n      expo-font: 14.0.8(expo@54.0.10)(react-native@0.81.4)(react@19.1.0)\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /@expo/ws-tunnel@1.0.6:\n    resolution: {integrity: sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==}\n    dev: false\n\n  /@expo/xcpretty@4.3.2:\n    resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==}\n    hasBin: true\n    dependencies:\n      '@babel/code-frame': 7.10.4\n      chalk: 4.1.2\n      find-up: 5.0.0\n      js-yaml: 4.1.0\n    dev: false\n\n  /@isaacs/cliui@8.0.2:\n    resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}\n    engines: {node: '>=12'}\n    dependencies:\n      string-width: 5.1.2\n      string-width-cjs: /string-width@4.2.3\n      strip-ansi: 7.1.2\n      strip-ansi-cjs: /strip-ansi@6.0.1\n      wrap-ansi: 8.1.0\n      wrap-ansi-cjs: /wrap-ansi@7.0.0\n    dev: false\n\n  /@isaacs/fs-minipass@4.0.1:\n    resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}\n    engines: {node: '>=18.0.0'}\n    dependencies:\n      minipass: 7.1.2\n    dev: false\n\n  /@isaacs/ttlcache@1.4.1:\n    resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==}\n    engines: {node: '>=12'}\n    dev: false\n\n  /@istanbuljs/load-nyc-config@1.1.0:\n    resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}\n    engines: {node: '>=8'}\n    dependencies:\n      camelcase: 5.3.1\n      find-up: 4.1.0\n      get-package-type: 0.1.0\n      js-yaml: 3.14.1\n      resolve-from: 5.0.0\n    dev: false\n\n  /@istanbuljs/schema@0.1.3:\n    resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /@jest/create-cache-key-function@29.7.0:\n    resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/types': 29.6.3\n    dev: false\n\n  /@jest/environment@29.7.0:\n    resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/fake-timers': 29.7.0\n      '@jest/types': 29.6.3\n      '@types/node': 24.5.2\n      jest-mock: 29.7.0\n    dev: false\n\n  /@jest/fake-timers@29.7.0:\n    resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/types': 29.6.3\n      '@sinonjs/fake-timers': 10.3.0\n      '@types/node': 24.5.2\n      jest-message-util: 29.7.0\n      jest-mock: 29.7.0\n      jest-util: 29.7.0\n    dev: false\n\n  /@jest/schemas@29.6.3:\n    resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@sinclair/typebox': 0.27.8\n    dev: false\n\n  /@jest/transform@29.7.0:\n    resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@babel/core': 7.28.4\n      '@jest/types': 29.6.3\n      '@jridgewell/trace-mapping': 0.3.31\n      babel-plugin-istanbul: 6.1.1\n      chalk: 4.1.2\n      convert-source-map: 2.0.0\n      fast-json-stable-stringify: 2.1.0\n      graceful-fs: 4.2.11\n      jest-haste-map: 29.7.0\n      jest-regex-util: 29.6.3\n      jest-util: 29.7.0\n      micromatch: 4.0.8\n      pirates: 4.0.7\n      slash: 3.0.0\n      write-file-atomic: 4.0.2\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@jest/types@29.6.3:\n    resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/schemas': 29.6.3\n      '@types/istanbul-lib-coverage': 2.0.6\n      '@types/istanbul-reports': 3.0.4\n      '@types/node': 24.5.2\n      '@types/yargs': 17.0.33\n      chalk: 4.1.2\n    dev: false\n\n  /@jridgewell/gen-mapping@0.3.13:\n    resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}\n    dependencies:\n      '@jridgewell/sourcemap-codec': 1.5.5\n      '@jridgewell/trace-mapping': 0.3.31\n    dev: false\n\n  /@jridgewell/remapping@2.3.5:\n    resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}\n    dependencies:\n      '@jridgewell/gen-mapping': 0.3.13\n      '@jridgewell/trace-mapping': 0.3.31\n    dev: false\n\n  /@jridgewell/resolve-uri@3.1.2:\n    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}\n    engines: {node: '>=6.0.0'}\n    dev: false\n\n  /@jridgewell/source-map@0.3.11:\n    resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}\n    dependencies:\n      '@jridgewell/gen-mapping': 0.3.13\n      '@jridgewell/trace-mapping': 0.3.31\n    dev: false\n\n  /@jridgewell/sourcemap-codec@1.5.5:\n    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}\n    dev: false\n\n  /@jridgewell/trace-mapping@0.3.31:\n    resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}\n    dependencies:\n      '@jridgewell/resolve-uri': 3.1.2\n      '@jridgewell/sourcemap-codec': 1.5.5\n    dev: false\n\n  /@pkgjs/parseargs@0.11.0:\n    resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}\n    engines: {node: '>=14'}\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /@radix-ui/primitive@1.1.3:\n    resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}\n    dev: false\n\n  /@radix-ui/react-collection@1.1.7(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-context': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-slot': 1.2.3(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-context@1.1.2(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-dialog@1.1.15(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/primitive': 1.1.3\n      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-context': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-dismissable-layer': 1.1.11(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-focus-scope': 1.1.7(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-id': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-portal': 1.1.9(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-presence': 1.1.5(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-slot': 1.2.3(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      aria-hidden: 1.2.6\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n      react-remove-scroll: 2.7.1(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-direction@1.1.1(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-dismissable-layer@1.1.11(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/primitive': 1.1.3\n      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-focus-scope@1.1.7(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-id@1.1.1(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-portal@1.1.9(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-presence@1.1.5(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-primitive@2.1.3(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/react-slot': 1.2.3(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-roving-focus@1.1.11(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/primitive': 1.1.3\n      '@radix-ui/react-collection': 1.1.7(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-context': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-direction': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-id': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-slot@1.2.0(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-slot@1.2.3(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-tabs@1.1.13(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}\n    peerDependencies:\n      '@types/react': '*'\n      '@types/react-dom': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n      '@types/react-dom':\n        optional: true\n    dependencies:\n      '@radix-ui/primitive': 1.1.3\n      '@radix-ui/react-context': 1.1.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-direction': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-id': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-presence': 1.1.5(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-roving-focus': 1.1.11(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    dev: false\n\n  /@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.13)(react@19.1.0)\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n    dev: false\n\n  /@react-buoy/core@0.1.12(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-WspgcZiuXHfXl9BT8KT2r9FEsy4wrxHS5AtuRAj/z27miijKmgCVfJAO3MlNYDIM1HMz8GghnySjGn/pOcik6g==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      '@react-buoy/shared-ui': 0.1.12(react-native@0.81.4)(react@19.1.0)\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /@react-buoy/react-query@0.1.12(@tanstack/react-query@5.90.2)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-kWB/NDLPWtwgHeQH1XJBCeGt6UKgyHBXrZwY3GjirITWicFMErKl3DFqt4FiCnj3kiPm+iSm1VQGXYGd3ZAYAw==}\n    requiresBuild: true\n    peerDependencies:\n      '@tanstack/react-query': '>=5.0.0'\n      react: '*'\n      react-native: '*'\n    dependencies:\n      '@react-buoy/shared-ui': 0.1.12(react-native@0.81.4)(react@19.1.0)\n      '@tanstack/react-query': 5.90.2(react@19.1.0)\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /@react-buoy/shared-ui@0.1.12(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-zSXb41OEEBquqe8RROxVc6YjN5uqSstR20lE5PtK3JK0RbTD6GG/uonFzqEd+xEwe5b+o7nbXUaoBj80dfULzw==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      superjson: 2.2.2\n    dev: false\n\n  /@react-native-async-storage/async-storage@2.2.0(react-native@0.81.4):\n    resolution: {integrity: sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==}\n    peerDependencies:\n      react-native: ^0.0.0-0 || >=0.65 <1.0\n    dependencies:\n      merge-options: 3.0.4\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /@react-native-community/netinfo@11.4.1(react-native@0.81.4):\n    resolution: {integrity: sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==}\n    peerDependencies:\n      react-native: '>=0.59'\n    dependencies:\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /@react-native/assets-registry@0.81.4:\n    resolution: {integrity: sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA==}\n    engines: {node: '>= 20.19.4'}\n    dev: false\n\n  /@react-native/babel-plugin-codegen@0.81.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-6ztXf2Tl2iWznyI/Da/N2Eqymt0Mnn69GCLnEFxFbNdk0HxHPZBNWU9shTXhsLWOL7HATSqwg/bB1+3kY1q+mA==}\n    engines: {node: '>= 20.19.4'}\n    dependencies:\n      '@babel/traverse': 7.28.4\n      '@react-native/codegen': 0.81.4(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - '@babel/core'\n      - supports-color\n    dev: false\n\n  /@react-native/babel-preset@0.81.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-VYj0c/cTjQJn/RJ5G6P0L9wuYSbU9yGbPYDHCKstlQZQWkk+L9V8ZDbxdJBTIei9Xl3KPQ1odQ4QaeW+4v+AZg==}\n    engines: {node: '>= 20.19.4'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.4)\n      '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-block-scoping': 7.28.4(@babel/core@7.28.4)\n      '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.4)\n      '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4)\n      '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.28.4)\n      '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4)\n      '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.4)\n      '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.28.4)\n      '@babel/plugin-transform-runtime': 7.28.3(@babel/core@7.28.4)\n      '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4)\n      '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.4)\n      '@babel/template': 7.27.2\n      '@react-native/babel-plugin-codegen': 0.81.4(@babel/core@7.28.4)\n      babel-plugin-syntax-hermes-parser: 0.29.1\n      babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.4)\n      react-refresh: 0.14.2\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@react-native/codegen@0.81.4(@babel/core@7.28.4):\n    resolution: {integrity: sha512-LWTGUTzFu+qOQnvkzBP52B90Ym3stZT8IFCzzUrppz8Iwglg83FCtDZAR4yLHI29VY/x/+pkcWAMCl3739XHdw==}\n    engines: {node: '>= 20.19.4'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/parser': 7.28.4\n      glob: 7.2.3\n      hermes-parser: 0.29.1\n      invariant: 2.2.4\n      nullthrows: 1.1.1\n      yargs: 17.7.2\n    dev: false\n\n  /@react-native/community-cli-plugin@0.81.4:\n    resolution: {integrity: sha512-8mpnvfcLcnVh+t1ok6V9eozWo8Ut+TZhz8ylJ6gF9d6q9EGDQX6s8jenan5Yv/pzN4vQEKI4ib2pTf/FELw+SA==}\n    engines: {node: '>= 20.19.4'}\n    peerDependencies:\n      '@react-native-community/cli': '*'\n      '@react-native/metro-config': '*'\n    peerDependenciesMeta:\n      '@react-native-community/cli':\n        optional: true\n      '@react-native/metro-config':\n        optional: true\n    dependencies:\n      '@react-native/dev-middleware': 0.81.4\n      debug: 4.4.3\n      invariant: 2.2.4\n      metro: 0.83.2\n      metro-config: 0.83.2\n      metro-core: 0.83.2\n      semver: 7.7.2\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /@react-native/debugger-frontend@0.81.4:\n    resolution: {integrity: sha512-SU05w1wD0nKdQFcuNC9D6De0ITnINCi8MEnx9RsTD2e4wN83ukoC7FpXaPCYyP6+VjFt5tUKDPgP1O7iaNXCqg==}\n    engines: {node: '>= 20.19.4'}\n    dev: false\n\n  /@react-native/dev-middleware@0.81.4:\n    resolution: {integrity: sha512-hu1Wu5R28FT7nHXs2wWXvQ++7W7zq5GPY83llajgPlYKznyPLAY/7bArc5rAzNB7b0kwnlaoPQKlvD/VP9LZug==}\n    engines: {node: '>= 20.19.4'}\n    dependencies:\n      '@isaacs/ttlcache': 1.4.1\n      '@react-native/debugger-frontend': 0.81.4\n      chrome-launcher: 0.15.2\n      chromium-edge-launcher: 0.2.0\n      connect: 3.7.0\n      debug: 4.4.3\n      invariant: 2.2.4\n      nullthrows: 1.1.1\n      open: 7.4.2\n      serve-static: 1.16.2\n      ws: 6.2.3\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /@react-native/gradle-plugin@0.81.4:\n    resolution: {integrity: sha512-T7fPcQvDDCSusZFVSg6H1oVDKb/NnVYLnsqkcHsAF2C2KGXyo3J7slH/tJAwNfj/7EOA2OgcWxfC1frgn9TQvw==}\n    engines: {node: '>= 20.19.4'}\n    dev: false\n\n  /@react-native/js-polyfills@0.81.4:\n    resolution: {integrity: sha512-sr42FaypKXJHMVHhgSbu2f/ZJfrLzgaoQ+HdpRvKEiEh2mhFf6XzZwecyLBvWqf2pMPZa+CpPfNPiejXjKEy8w==}\n    engines: {node: '>= 20.19.4'}\n    dev: false\n\n  /@react-native/normalize-colors@0.81.4:\n    resolution: {integrity: sha512-9nRRHO1H+tcFqjb9gAM105Urtgcanbta2tuqCVY0NATHeFPDEAB7gPyiLxCHKMi1NbhP6TH0kxgSWXKZl1cyRg==}\n    dev: false\n\n  /@react-native/virtualized-lists@0.81.4(@types/react@19.1.13)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-hBM+rMyL6Wm1Q4f/WpqGsaCojKSNUBqAXLABNGoWm1vabZ7cSnARMxBvA/2vo3hLcoR4v7zDK8tkKm9+O0LjVA==}\n    engines: {node: '>= 20.19.4'}\n    peerDependencies:\n      '@types/react': ^19.1.0\n      react: '*'\n      react-native: '*'\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      invariant: 2.2.4\n      nullthrows: 1.1.1\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /@react-navigation/bottom-tabs@7.4.7(@react-navigation/native@7.1.17)(react-native-safe-area-context@5.6.1)(react-native-screens@4.16.0)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-SQ4KuYV9yr3SV/thefpLWhAD0CU2CrBMG1l0w/QKl3GYuGWdN5OQmdQdmaPZGtsjjVOb+N9Qo7Tf6210P4TlpA==}\n    peerDependencies:\n      '@react-navigation/native': ^7.1.17\n      react: '>= 18.2.0'\n      react-native: '*'\n      react-native-safe-area-context: '>= 4.0.0'\n      react-native-screens: '>= 4.0.0'\n    dependencies:\n      '@react-navigation/elements': 2.6.4(@react-navigation/native@7.1.17)(react-native-safe-area-context@5.6.1)(react-native@0.81.4)(react@19.1.0)\n      '@react-navigation/native': 7.1.17(react-native@0.81.4)(react@19.1.0)\n      color: 4.2.3\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-native-safe-area-context: 5.6.1(react-native@0.81.4)(react@19.1.0)\n      react-native-screens: 4.16.0(react-native@0.81.4)(react@19.1.0)\n    transitivePeerDependencies:\n      - '@react-native-masked-view/masked-view'\n    dev: false\n\n  /@react-navigation/core@7.12.4(react@19.1.0):\n    resolution: {integrity: sha512-xLFho76FA7v500XID5z/8YfGTvjQPw7/fXsq4BIrVSqetNe/o/v+KAocEw4ots6kyv3XvSTyiWKh2g3pN6xZ9Q==}\n    peerDependencies:\n      react: '>= 18.2.0'\n    dependencies:\n      '@react-navigation/routers': 7.5.1\n      escape-string-regexp: 4.0.0\n      nanoid: 3.3.11\n      query-string: 7.1.3\n      react: 19.1.0\n      react-is: 19.1.1\n      use-latest-callback: 0.2.4(react@19.1.0)\n      use-sync-external-store: 1.5.0(react@19.1.0)\n    dev: false\n\n  /@react-navigation/elements@2.6.4(@react-navigation/native@7.1.17)(react-native-safe-area-context@5.6.1)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-O3X9vWXOEhAO56zkQS7KaDzL8BvjlwZ0LGSteKpt1/k6w6HONG+2Wkblrb057iKmehTkEkQMzMLkXiuLmN5x9Q==}\n    peerDependencies:\n      '@react-native-masked-view/masked-view': '>= 0.2.0'\n      '@react-navigation/native': ^7.1.17\n      react: '>= 18.2.0'\n      react-native: '*'\n      react-native-safe-area-context: '>= 4.0.0'\n    peerDependenciesMeta:\n      '@react-native-masked-view/masked-view':\n        optional: true\n    dependencies:\n      '@react-navigation/native': 7.1.17(react-native@0.81.4)(react@19.1.0)\n      color: 4.2.3\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-native-safe-area-context: 5.6.1(react-native@0.81.4)(react@19.1.0)\n      use-latest-callback: 0.2.4(react@19.1.0)\n      use-sync-external-store: 1.5.0(react@19.1.0)\n    dev: false\n\n  /@react-navigation/native-stack@7.3.26(@react-navigation/native@7.1.17)(react-native-safe-area-context@5.6.1)(react-native-screens@4.16.0)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-EjaBWzLZ76HJGOOcWCFf+h/M+Zg7M1RalYioDOb6ZdXHz7AwYNidruT3OUAQgSzg3gVLqvu5OYO0jFsNDPCZxQ==}\n    peerDependencies:\n      '@react-navigation/native': ^7.1.17\n      react: '>= 18.2.0'\n      react-native: '*'\n      react-native-safe-area-context: '>= 4.0.0'\n      react-native-screens: '>= 4.0.0'\n    dependencies:\n      '@react-navigation/elements': 2.6.4(@react-navigation/native@7.1.17)(react-native-safe-area-context@5.6.1)(react-native@0.81.4)(react@19.1.0)\n      '@react-navigation/native': 7.1.17(react-native@0.81.4)(react@19.1.0)\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-native-safe-area-context: 5.6.1(react-native@0.81.4)(react@19.1.0)\n      react-native-screens: 4.16.0(react-native@0.81.4)(react@19.1.0)\n      warn-once: 0.1.1\n    transitivePeerDependencies:\n      - '@react-native-masked-view/masked-view'\n    dev: false\n\n  /@react-navigation/native@7.1.17(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-uEcYWi1NV+2Qe1oELfp9b5hTYekqWATv2cuwcOAg5EvsIsUPtzFrKIasgUXLBRGb9P7yR5ifoJ+ug4u6jdqSTQ==}\n    peerDependencies:\n      react: '>= 18.2.0'\n      react-native: '*'\n    dependencies:\n      '@react-navigation/core': 7.12.4(react@19.1.0)\n      escape-string-regexp: 4.0.0\n      fast-deep-equal: 3.1.3\n      nanoid: 3.3.11\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      use-latest-callback: 0.2.4(react@19.1.0)\n    dev: false\n\n  /@react-navigation/routers@7.5.1:\n    resolution: {integrity: sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==}\n    dependencies:\n      nanoid: 3.3.11\n    dev: false\n\n  /@sinclair/typebox@0.27.8:\n    resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}\n    dev: false\n\n  /@sinonjs/commons@3.0.1:\n    resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}\n    dependencies:\n      type-detect: 4.0.8\n    dev: false\n\n  /@sinonjs/fake-timers@10.3.0:\n    resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}\n    dependencies:\n      '@sinonjs/commons': 3.0.1\n    dev: false\n\n  /@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n    dev: false\n\n  /@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n    dev: false\n\n  /@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n    dev: false\n\n  /@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n    dev: false\n\n  /@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n    dev: false\n\n  /@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n    dev: false\n\n  /@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n    dev: false\n\n  /@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==}\n    engines: {node: '>=12'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n    dev: false\n\n  /@svgr/babel-preset@8.1.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.4)\n      '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.4)\n      '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.4)\n      '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.4)\n      '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.4)\n      '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.4)\n      '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.4)\n      '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.4)\n    dev: false\n\n  /@svgr/core@8.1.0(typescript@5.9.2):\n    resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==}\n    engines: {node: '>=14'}\n    dependencies:\n      '@babel/core': 7.28.4\n      '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4)\n      camelcase: 6.3.0\n      cosmiconfig: 8.3.6(typescript@5.9.2)\n      snake-case: 3.0.4\n    transitivePeerDependencies:\n      - supports-color\n      - typescript\n    dev: false\n\n  /@svgr/hast-util-to-babel-ast@8.0.0:\n    resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==}\n    engines: {node: '>=14'}\n    dependencies:\n      '@babel/types': 7.28.4\n      entities: 4.5.0\n    dev: false\n\n  /@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0):\n    resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@svgr/core': '*'\n    dependencies:\n      '@babel/core': 7.28.4\n      '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4)\n      '@svgr/core': 8.1.0(typescript@5.9.2)\n      '@svgr/hast-util-to-babel-ast': 8.0.0\n      svg-parser: 2.0.4\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0)(typescript@5.9.2):\n    resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      '@svgr/core': '*'\n    dependencies:\n      '@svgr/core': 8.1.0(typescript@5.9.2)\n      cosmiconfig: 8.3.6(typescript@5.9.2)\n      deepmerge: 4.3.1\n      svgo: 3.3.2\n    transitivePeerDependencies:\n      - typescript\n    dev: false\n\n  /@tanstack/query-async-storage-persister@5.90.2:\n    resolution: {integrity: sha512-oyb7IHW85hsRdSZZNPu5dowQJFX3agOR/1O4M6Qc5V7s5dfnex5CqipEu0tbScNRMc2knna2ihQUiEQuTXWrEQ==}\n    dependencies:\n      '@tanstack/query-core': 5.90.2\n      '@tanstack/query-persist-client-core': 5.90.2\n    dev: false\n\n  /@tanstack/query-core@5.90.2:\n    resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==}\n    dev: false\n\n  /@tanstack/query-persist-client-core@5.90.2:\n    resolution: {integrity: sha512-rgJRgqqziPc3KgK2mav2HNR4PoI5e7fkiIrkg85xZ5j29mHPzTp3A0QcceQXVaV9qcPp/SMDJA48A6BpGJGHZg==}\n    dependencies:\n      '@tanstack/query-core': 5.90.2\n    dev: false\n\n  /@tanstack/react-query-persist-client@5.90.2(@tanstack/react-query@5.90.2)(react@19.1.0):\n    resolution: {integrity: sha512-ii5VbUlxv/zSPWbMT5Sr7VAsmvjup+xu7XeHj/umRiZ3cR7Ulc+6qwFhfehw7sUi1L6/K0RN5hXZjtzPBrUgjA==}\n    peerDependencies:\n      '@tanstack/react-query': ^5.90.2\n      react: ^18 || ^19\n    dependencies:\n      '@tanstack/query-persist-client-core': 5.90.2\n      '@tanstack/react-query': 5.90.2(react@19.1.0)\n      react: 19.1.0\n    dev: false\n\n  /@tanstack/react-query@5.90.2(react@19.1.0):\n    resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==}\n    peerDependencies:\n      react: ^18 || ^19\n    dependencies:\n      '@tanstack/query-core': 5.90.2\n      react: 19.1.0\n    dev: false\n\n  /@trysound/sax@0.2.0:\n    resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}\n    engines: {node: '>=10.13.0'}\n    dev: false\n\n  /@types/babel__core@7.20.5:\n    resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}\n    dependencies:\n      '@babel/parser': 7.28.4\n      '@babel/types': 7.28.4\n      '@types/babel__generator': 7.27.0\n      '@types/babel__template': 7.4.4\n      '@types/babel__traverse': 7.28.0\n    dev: false\n\n  /@types/babel__generator@7.27.0:\n    resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}\n    dependencies:\n      '@babel/types': 7.28.4\n    dev: false\n\n  /@types/babel__template@7.4.4:\n    resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}\n    dependencies:\n      '@babel/parser': 7.28.4\n      '@babel/types': 7.28.4\n    dev: false\n\n  /@types/babel__traverse@7.28.0:\n    resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}\n    dependencies:\n      '@babel/types': 7.28.4\n    dev: false\n\n  /@types/graceful-fs@4.1.9:\n    resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}\n    dependencies:\n      '@types/node': 24.5.2\n    dev: false\n\n  /@types/hammerjs@2.0.46:\n    resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==}\n    dev: false\n\n  /@types/istanbul-lib-coverage@2.0.6:\n    resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}\n    dev: false\n\n  /@types/istanbul-lib-report@3.0.3:\n    resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==}\n    dependencies:\n      '@types/istanbul-lib-coverage': 2.0.6\n    dev: false\n\n  /@types/istanbul-reports@3.0.4:\n    resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}\n    dependencies:\n      '@types/istanbul-lib-report': 3.0.3\n    dev: false\n\n  /@types/node@24.5.2:\n    resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==}\n    dependencies:\n      undici-types: 7.12.0\n    dev: false\n\n  /@types/react@19.1.13:\n    resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==}\n    dependencies:\n      csstype: 3.1.3\n\n  /@types/stack-utils@2.0.3:\n    resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}\n    dev: false\n\n  /@types/yargs-parser@21.0.3:\n    resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}\n    dev: false\n\n  /@types/yargs@17.0.33:\n    resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}\n    dependencies:\n      '@types/yargs-parser': 21.0.3\n    dev: false\n\n  /@ungap/structured-clone@1.3.0:\n    resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}\n    dev: false\n\n  /@urql/core@5.2.0:\n    resolution: {integrity: sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==}\n    dependencies:\n      '@0no-co/graphql.web': 1.2.0\n      wonka: 6.3.5\n    transitivePeerDependencies:\n      - graphql\n    dev: false\n\n  /@urql/exchange-retry@1.3.2(@urql/core@5.2.0):\n    resolution: {integrity: sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==}\n    peerDependencies:\n      '@urql/core': ^5.0.0\n    dependencies:\n      '@urql/core': 5.2.0\n      wonka: 6.3.5\n    dev: false\n\n  /@xmldom/xmldom@0.8.11:\n    resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}\n    engines: {node: '>=10.0.0'}\n    dev: false\n\n  /abort-controller@3.0.0:\n    resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}\n    engines: {node: '>=6.5'}\n    dependencies:\n      event-target-shim: 5.0.1\n    dev: false\n\n  /accepts@1.3.8:\n    resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}\n    engines: {node: '>= 0.6'}\n    dependencies:\n      mime-types: 2.1.35\n      negotiator: 0.6.3\n    dev: false\n\n  /acorn@8.15.0:\n    resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}\n    engines: {node: '>=0.4.0'}\n    hasBin: true\n    dev: false\n\n  /agent-base@7.1.4:\n    resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}\n    engines: {node: '>= 14'}\n    dev: false\n\n  /anser@1.4.10:\n    resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==}\n    dev: false\n\n  /ansi-escapes@4.3.2:\n    resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}\n    engines: {node: '>=8'}\n    dependencies:\n      type-fest: 0.21.3\n    dev: false\n\n  /ansi-regex@4.1.1:\n    resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /ansi-regex@5.0.1:\n    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /ansi-regex@6.2.2:\n    resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}\n    engines: {node: '>=12'}\n    dev: false\n\n  /ansi-styles@3.2.1:\n    resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}\n    engines: {node: '>=4'}\n    dependencies:\n      color-convert: 1.9.3\n    dev: false\n\n  /ansi-styles@4.3.0:\n    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}\n    engines: {node: '>=8'}\n    dependencies:\n      color-convert: 2.0.1\n    dev: false\n\n  /ansi-styles@5.2.0:\n    resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /ansi-styles@6.2.3:\n    resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}\n    engines: {node: '>=12'}\n    dev: false\n\n  /any-promise@1.3.0:\n    resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}\n    dev: false\n\n  /anymatch@3.1.3:\n    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}\n    engines: {node: '>= 8'}\n    dependencies:\n      normalize-path: 3.0.0\n      picomatch: 2.3.1\n    dev: false\n\n  /arg@5.0.2:\n    resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}\n    dev: false\n\n  /argparse@1.0.10:\n    resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}\n    dependencies:\n      sprintf-js: 1.0.3\n    dev: false\n\n  /argparse@2.0.1:\n    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}\n    dev: false\n\n  /aria-hidden@1.2.6:\n    resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}\n    engines: {node: '>=10'}\n    dependencies:\n      tslib: 2.8.1\n    dev: false\n\n  /asap@2.0.6:\n    resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}\n    dev: false\n\n  /async-limiter@1.0.1:\n    resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}\n    dev: false\n\n  /asynckit@0.4.0:\n    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}\n    dev: false\n\n  /axios@1.12.2:\n    resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}\n    dependencies:\n      follow-redirects: 1.15.11\n      form-data: 4.0.4\n      proxy-from-env: 1.1.0\n    transitivePeerDependencies:\n      - debug\n    dev: false\n\n  /babel-jest@29.7.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@jest/transform': 29.7.0\n      '@types/babel__core': 7.20.5\n      babel-plugin-istanbul: 6.1.1\n      babel-preset-jest: 29.6.3(@babel/core@7.28.4)\n      chalk: 4.1.2\n      graceful-fs: 4.2.11\n      slash: 3.0.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /babel-plugin-istanbul@6.1.1:\n    resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==}\n    engines: {node: '>=8'}\n    dependencies:\n      '@babel/helper-plugin-utils': 7.27.1\n      '@istanbuljs/load-nyc-config': 1.1.0\n      '@istanbuljs/schema': 0.1.3\n      istanbul-lib-instrument: 5.2.1\n      test-exclude: 6.0.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /babel-plugin-jest-hoist@29.6.3:\n    resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@babel/template': 7.27.2\n      '@babel/types': 7.28.4\n      '@types/babel__core': 7.20.5\n      '@types/babel__traverse': 7.28.0\n    dev: false\n\n  /babel-plugin-module-resolver@5.0.2:\n    resolution: {integrity: sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==}\n    dependencies:\n      find-babel-config: 2.1.2\n      glob: 9.3.5\n      pkg-up: 3.1.0\n      reselect: 4.1.8\n      resolve: 1.22.10\n    dev: false\n\n  /babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4):\n    resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/compat-data': 7.28.4\n      '@babel/core': 7.28.4\n      '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4)\n      semver: 6.3.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4)\n      core-js-compat: 3.45.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.4):\n    resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /babel-plugin-react-compiler@19.1.0-rc.3:\n    resolution: {integrity: sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==}\n    dependencies:\n      '@babel/types': 7.28.4\n    dev: false\n\n  /babel-plugin-react-native-web@0.21.1:\n    resolution: {integrity: sha512-7XywfJ5QIRMwjOL+pwJt2w47Jmi5fFLvK7/So4fV4jIN6PcRbylCp9/l3cJY4VJbSz3lnWTeHDTD1LKIc1C09Q==}\n    dev: false\n\n  /babel-plugin-syntax-hermes-parser@0.29.1:\n    resolution: {integrity: sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA==}\n    dependencies:\n      hermes-parser: 0.29.1\n    dev: false\n\n  /babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.28.4):\n    resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==}\n    dependencies:\n      '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.4)\n    transitivePeerDependencies:\n      - '@babel/core'\n    dev: false\n\n  /babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4):\n    resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4)\n      '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4)\n      '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4)\n      '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4)\n      '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4)\n      '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4)\n      '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4)\n      '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4)\n      '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4)\n    dev: false\n\n  /babel-preset-expo@54.0.3(@babel/core@7.28.4)(@babel/runtime@7.28.4)(expo@54.0.10)(react-refresh@0.14.2):\n    resolution: {integrity: sha512-zC6g96Mbf1bofnCI8yI0VKAp8/ER/gpfTsWOpQvStbHU+E4jFZ294n3unW8Hf6nNP4NoeNq9Zc6Prp0vwhxbow==}\n    peerDependencies:\n      '@babel/runtime': ^7.20.0\n      expo: '*'\n      react-refresh: '>=0.14.0 <1.0.0'\n    peerDependenciesMeta:\n      '@babel/runtime':\n        optional: true\n      expo:\n        optional: true\n    dependencies:\n      '@babel/helper-module-imports': 7.27.1\n      '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.4)\n      '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.28.4)\n      '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.28.4)\n      '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4)\n      '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-runtime': 7.28.3(@babel/core@7.28.4)\n      '@babel/preset-react': 7.27.1(@babel/core@7.28.4)\n      '@babel/preset-typescript': 7.27.1(@babel/core@7.28.4)\n      '@babel/runtime': 7.28.4\n      '@react-native/babel-preset': 0.81.4(@babel/core@7.28.4)\n      babel-plugin-react-compiler: 19.1.0-rc.3\n      babel-plugin-react-native-web: 0.21.1\n      babel-plugin-syntax-hermes-parser: 0.29.1\n      babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.4)\n      debug: 4.4.3\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      react-refresh: 0.14.2\n      resolve-from: 5.0.0\n    transitivePeerDependencies:\n      - '@babel/core'\n      - supports-color\n    dev: false\n\n  /babel-preset-jest@29.6.3(@babel/core@7.28.4):\n    resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    peerDependencies:\n      '@babel/core': 7.28.4\n    dependencies:\n      '@babel/core': 7.28.4\n      babel-plugin-jest-hoist: 29.6.3\n      babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4)\n    dev: false\n\n  /balanced-match@1.0.2:\n    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}\n    dev: false\n\n  /base64-js@1.5.1:\n    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}\n    dev: false\n\n  /baseline-browser-mapping@2.8.6:\n    resolution: {integrity: sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==}\n    hasBin: true\n    dev: false\n\n  /better-opn@3.0.2:\n    resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}\n    engines: {node: '>=12.0.0'}\n    dependencies:\n      open: 8.4.2\n    dev: false\n\n  /big-integer@1.6.52:\n    resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}\n    engines: {node: '>=0.6'}\n    dev: false\n\n  /boolbase@1.0.0:\n    resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}\n    dev: false\n\n  /bplist-creator@0.1.0:\n    resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==}\n    dependencies:\n      stream-buffers: 2.2.0\n    dev: false\n\n  /bplist-parser@0.3.1:\n    resolution: {integrity: sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==}\n    engines: {node: '>= 5.10.0'}\n    dependencies:\n      big-integer: 1.6.52\n    dev: false\n\n  /bplist-parser@0.3.2:\n    resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}\n    engines: {node: '>= 5.10.0'}\n    dependencies:\n      big-integer: 1.6.52\n    dev: false\n\n  /brace-expansion@1.1.12:\n    resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}\n    dependencies:\n      balanced-match: 1.0.2\n      concat-map: 0.0.1\n    dev: false\n\n  /brace-expansion@2.0.2:\n    resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}\n    dependencies:\n      balanced-match: 1.0.2\n    dev: false\n\n  /braces@3.0.3:\n    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}\n    engines: {node: '>=8'}\n    dependencies:\n      fill-range: 7.1.1\n    dev: false\n\n  /browserslist@4.26.2:\n    resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==}\n    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}\n    hasBin: true\n    dependencies:\n      baseline-browser-mapping: 2.8.6\n      caniuse-lite: 1.0.30001743\n      electron-to-chromium: 1.5.222\n      node-releases: 2.0.21\n      update-browserslist-db: 1.1.3(browserslist@4.26.2)\n    dev: false\n\n  /bser@2.1.1:\n    resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}\n    dependencies:\n      node-int64: 0.4.0\n    dev: false\n\n  /buffer-from@1.1.2:\n    resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}\n    dev: false\n\n  /buffer@5.7.1:\n    resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}\n    dependencies:\n      base64-js: 1.5.1\n      ieee754: 1.2.1\n    dev: false\n\n  /bytes@3.1.2:\n    resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /call-bind-apply-helpers@1.0.2:\n    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      es-errors: 1.3.0\n      function-bind: 1.1.2\n    dev: false\n\n  /caller-callsite@2.0.0:\n    resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==}\n    engines: {node: '>=4'}\n    dependencies:\n      callsites: 2.0.0\n    dev: false\n\n  /caller-path@2.0.0:\n    resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==}\n    engines: {node: '>=4'}\n    dependencies:\n      caller-callsite: 2.0.0\n    dev: false\n\n  /callsites@2.0.0:\n    resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /callsites@3.1.0:\n    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /camelcase@5.3.1:\n    resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /camelcase@6.3.0:\n    resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /caniuse-lite@1.0.30001743:\n    resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==}\n    dev: false\n\n  /chalk@2.4.2:\n    resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}\n    engines: {node: '>=4'}\n    dependencies:\n      ansi-styles: 3.2.1\n      escape-string-regexp: 1.0.5\n      supports-color: 5.5.0\n    dev: false\n\n  /chalk@4.1.2:\n    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}\n    engines: {node: '>=10'}\n    dependencies:\n      ansi-styles: 4.3.0\n      supports-color: 7.2.0\n    dev: false\n\n  /chownr@3.0.0:\n    resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}\n    engines: {node: '>=18'}\n    dev: false\n\n  /chrome-launcher@0.15.2:\n    resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==}\n    engines: {node: '>=12.13.0'}\n    hasBin: true\n    dependencies:\n      '@types/node': 24.5.2\n      escape-string-regexp: 4.0.0\n      is-wsl: 2.2.0\n      lighthouse-logger: 1.4.2\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /chromium-edge-launcher@0.2.0:\n    resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==}\n    dependencies:\n      '@types/node': 24.5.2\n      escape-string-regexp: 4.0.0\n      is-wsl: 2.2.0\n      lighthouse-logger: 1.4.2\n      mkdirp: 1.0.4\n      rimraf: 3.0.2\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /ci-info@2.0.0:\n    resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}\n    dev: false\n\n  /ci-info@3.9.0:\n    resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /cli-cursor@2.1.0:\n    resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==}\n    engines: {node: '>=4'}\n    dependencies:\n      restore-cursor: 2.0.0\n    dev: false\n\n  /cli-spinners@2.9.2:\n    resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /client-only@0.0.1:\n    resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}\n    dev: false\n\n  /cliui@8.0.1:\n    resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}\n    engines: {node: '>=12'}\n    dependencies:\n      string-width: 4.2.3\n      strip-ansi: 6.0.1\n      wrap-ansi: 7.0.0\n    dev: false\n\n  /clone@1.0.4:\n    resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}\n    engines: {node: '>=0.8'}\n    dev: false\n\n  /color-convert@1.9.3:\n    resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}\n    dependencies:\n      color-name: 1.1.3\n    dev: false\n\n  /color-convert@2.0.1:\n    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}\n    engines: {node: '>=7.0.0'}\n    dependencies:\n      color-name: 1.1.4\n    dev: false\n\n  /color-name@1.1.3:\n    resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}\n    dev: false\n\n  /color-name@1.1.4:\n    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}\n    dev: false\n\n  /color-string@1.9.1:\n    resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}\n    dependencies:\n      color-name: 1.1.4\n      simple-swizzle: 0.2.4\n    dev: false\n\n  /color@4.2.3:\n    resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}\n    engines: {node: '>=12.5.0'}\n    dependencies:\n      color-convert: 2.0.1\n      color-string: 1.9.1\n    dev: false\n\n  /combined-stream@1.0.8:\n    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}\n    engines: {node: '>= 0.8'}\n    dependencies:\n      delayed-stream: 1.0.0\n    dev: false\n\n  /commander@12.1.0:\n    resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}\n    engines: {node: '>=18'}\n    dev: false\n\n  /commander@2.20.3:\n    resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}\n    dev: false\n\n  /commander@4.1.1:\n    resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}\n    engines: {node: '>= 6'}\n    dev: false\n\n  /commander@7.2.0:\n    resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}\n    engines: {node: '>= 10'}\n    dev: false\n\n  /compressible@2.0.18:\n    resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}\n    engines: {node: '>= 0.6'}\n    dependencies:\n      mime-db: 1.54.0\n    dev: false\n\n  /compression@1.8.1:\n    resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}\n    engines: {node: '>= 0.8.0'}\n    dependencies:\n      bytes: 3.1.2\n      compressible: 2.0.18\n      debug: 2.6.9\n      negotiator: 0.6.4\n      on-headers: 1.1.0\n      safe-buffer: 5.2.1\n      vary: 1.1.2\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /concat-map@0.0.1:\n    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}\n    dev: false\n\n  /connect@3.7.0:\n    resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}\n    engines: {node: '>= 0.10.0'}\n    dependencies:\n      debug: 2.6.9\n      finalhandler: 1.1.2\n      parseurl: 1.3.3\n      utils-merge: 1.0.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /convert-source-map@2.0.0:\n    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}\n    dev: false\n\n  /copy-anything@3.0.5:\n    resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}\n    engines: {node: '>=12.13'}\n    dependencies:\n      is-what: 4.1.16\n    dev: false\n\n  /core-js-compat@3.45.1:\n    resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==}\n    dependencies:\n      browserslist: 4.26.2\n    dev: false\n\n  /cosmiconfig@5.2.1:\n    resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==}\n    engines: {node: '>=4'}\n    dependencies:\n      import-fresh: 2.0.0\n      is-directory: 0.3.1\n      js-yaml: 3.14.1\n      parse-json: 4.0.0\n    dev: false\n\n  /cosmiconfig@8.3.6(typescript@5.9.2):\n    resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}\n    engines: {node: '>=14'}\n    peerDependencies:\n      typescript: '>=4.9.5'\n    peerDependenciesMeta:\n      typescript:\n        optional: true\n    dependencies:\n      import-fresh: 3.3.1\n      js-yaml: 4.1.0\n      parse-json: 5.2.0\n      path-type: 4.0.0\n      typescript: 5.9.2\n    dev: false\n\n  /cross-env@10.1.0:\n    resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}\n    engines: {node: '>=20'}\n    hasBin: true\n    dependencies:\n      '@epic-web/invariant': 1.0.0\n      cross-spawn: 7.0.6\n    dev: true\n\n  /cross-spawn@7.0.6:\n    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}\n    engines: {node: '>= 8'}\n    dependencies:\n      path-key: 3.1.1\n      shebang-command: 2.0.0\n      which: 2.0.2\n\n  /crypto-random-string@2.0.0:\n    resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /css-select@5.2.2:\n    resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}\n    dependencies:\n      boolbase: 1.0.0\n      css-what: 6.2.2\n      domhandler: 5.0.3\n      domutils: 3.2.2\n      nth-check: 2.1.1\n    dev: false\n\n  /css-tree@1.1.3:\n    resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}\n    engines: {node: '>=8.0.0'}\n    dependencies:\n      mdn-data: 2.0.14\n      source-map: 0.6.1\n    dev: false\n\n  /css-tree@2.2.1:\n    resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}\n    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}\n    dependencies:\n      mdn-data: 2.0.28\n      source-map-js: 1.2.1\n    dev: false\n\n  /css-tree@2.3.1:\n    resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}\n    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}\n    dependencies:\n      mdn-data: 2.0.30\n      source-map-js: 1.2.1\n    dev: false\n\n  /css-what@6.2.2:\n    resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}\n    engines: {node: '>= 6'}\n    dev: false\n\n  /csso@5.0.5:\n    resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}\n    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}\n    dependencies:\n      css-tree: 2.2.1\n    dev: false\n\n  /csstype@3.1.3:\n    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}\n\n  /debug@2.6.9:\n    resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}\n    peerDependencies:\n      supports-color: '*'\n    peerDependenciesMeta:\n      supports-color:\n        optional: true\n    dependencies:\n      ms: 2.0.0\n    dev: false\n\n  /debug@3.2.7:\n    resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}\n    peerDependencies:\n      supports-color: '*'\n    peerDependenciesMeta:\n      supports-color:\n        optional: true\n    dependencies:\n      ms: 2.1.3\n    dev: false\n\n  /debug@4.4.3:\n    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}\n    engines: {node: '>=6.0'}\n    peerDependencies:\n      supports-color: '*'\n    peerDependenciesMeta:\n      supports-color:\n        optional: true\n    dependencies:\n      ms: 2.1.3\n    dev: false\n\n  /decode-uri-component@0.2.2:\n    resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}\n    engines: {node: '>=0.10'}\n    dev: false\n\n  /deep-extend@0.6.0:\n    resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}\n    engines: {node: '>=4.0.0'}\n    dev: false\n\n  /deepmerge@4.3.1:\n    resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /defaults@1.0.4:\n    resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}\n    dependencies:\n      clone: 1.0.4\n    dev: false\n\n  /define-lazy-prop@2.0.0:\n    resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /delayed-stream@1.0.0:\n    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}\n    engines: {node: '>=0.4.0'}\n    dev: false\n\n  /depd@2.0.0:\n    resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /destroy@1.2.0:\n    resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}\n    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}\n    dev: false\n\n  /detect-libc@2.1.0:\n    resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /detect-node-es@1.1.0:\n    resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}\n    dev: false\n\n  /dom-serializer@2.0.0:\n    resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}\n    dependencies:\n      domelementtype: 2.3.0\n      domhandler: 5.0.3\n      entities: 4.5.0\n    dev: false\n\n  /domelementtype@2.3.0:\n    resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}\n    dev: false\n\n  /domhandler@5.0.3:\n    resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}\n    engines: {node: '>= 4'}\n    dependencies:\n      domelementtype: 2.3.0\n    dev: false\n\n  /domutils@3.2.2:\n    resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}\n    dependencies:\n      dom-serializer: 2.0.0\n      domelementtype: 2.3.0\n      domhandler: 5.0.3\n    dev: false\n\n  /dot-case@3.0.4:\n    resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}\n    dependencies:\n      no-case: 3.0.4\n      tslib: 2.8.1\n    dev: false\n\n  /dotenv-expand@11.0.7:\n    resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==}\n    engines: {node: '>=12'}\n    dependencies:\n      dotenv: 16.4.7\n    dev: false\n\n  /dotenv@16.4.7:\n    resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}\n    engines: {node: '>=12'}\n    dev: false\n\n  /dotenv@17.2.3:\n    resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}\n    engines: {node: '>=12'}\n    dev: false\n\n  /dunder-proto@1.0.1:\n    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      call-bind-apply-helpers: 1.0.2\n      es-errors: 1.3.0\n      gopd: 1.2.0\n    dev: false\n\n  /eastasianwidth@0.2.0:\n    resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}\n    dev: false\n\n  /ee-first@1.1.1:\n    resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}\n    dev: false\n\n  /electron-to-chromium@1.5.222:\n    resolution: {integrity: sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==}\n    dev: false\n\n  /emoji-regex@8.0.0:\n    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}\n    dev: false\n\n  /emoji-regex@9.2.2:\n    resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}\n    dev: false\n\n  /encodeurl@1.0.2:\n    resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /encodeurl@2.0.0:\n    resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /entities@4.5.0:\n    resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}\n    engines: {node: '>=0.12'}\n    dev: false\n\n  /env-editor@0.4.2:\n    resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /error-ex@1.3.4:\n    resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}\n    dependencies:\n      is-arrayish: 0.2.1\n    dev: false\n\n  /error-stack-parser@2.1.4:\n    resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}\n    dependencies:\n      stackframe: 1.3.4\n    dev: false\n\n  /es-define-property@1.0.1:\n    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}\n    engines: {node: '>= 0.4'}\n    dev: false\n\n  /es-errors@1.3.0:\n    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}\n    engines: {node: '>= 0.4'}\n    dev: false\n\n  /es-object-atoms@1.1.1:\n    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      es-errors: 1.3.0\n    dev: false\n\n  /es-set-tostringtag@2.1.0:\n    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      es-errors: 1.3.0\n      get-intrinsic: 1.3.0\n      has-tostringtag: 1.0.2\n      hasown: 2.0.2\n    dev: false\n\n  /escalade@3.2.0:\n    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /escape-html@1.0.3:\n    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}\n    dev: false\n\n  /escape-string-regexp@1.0.5:\n    resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}\n    engines: {node: '>=0.8.0'}\n    dev: false\n\n  /escape-string-regexp@2.0.0:\n    resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /escape-string-regexp@4.0.0:\n    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /esprima@4.0.1:\n    resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}\n    engines: {node: '>=4'}\n    hasBin: true\n    dev: false\n\n  /etag@1.8.1:\n    resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}\n    engines: {node: '>= 0.6'}\n    dev: false\n\n  /event-target-shim@5.0.1:\n    resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /exec-async@2.2.0:\n    resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==}\n    dev: false\n\n  /expo-asset@12.0.9(expo@54.0.10)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-vrdRoyhGhBmd0nJcssTSk1Ypx3Mbn/eXaaBCQVkL0MJ8IOZpAObAjfD5CTy8+8RofcHEQdh3wwZVCs7crvfOeg==}\n    peerDependencies:\n      expo: '*'\n      react: '*'\n      react-native: '*'\n    dependencies:\n      '@expo/image-utils': 0.8.7\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      expo-constants: 18.0.9(expo@54.0.10)(react-native@0.81.4)\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /expo-constants@18.0.9(expo@54.0.10)(react-native@0.81.4):\n    resolution: {integrity: sha512-sqoXHAOGDcr+M9NlXzj1tGoZyd3zxYDy215W6E0Z0n8fgBaqce9FAYQE2bu5X4G629AYig5go7U6sQz7Pjcm8A==}\n    peerDependencies:\n      expo: '*'\n      react-native: '*'\n    dependencies:\n      '@expo/config': 12.0.9\n      '@expo/env': 2.0.7\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /expo-file-system@19.0.15(expo@54.0.10)(react-native@0.81.4):\n    resolution: {integrity: sha512-sRLW+3PVJDiuoCE2LuteHhC7OxPjh1cfqLylf1YG1TDEbbQXnzwjfsKeRm6dslEPZLkMWfSLYIrVbnuq5mF7kQ==}\n    peerDependencies:\n      expo: '*'\n      react-native: '*'\n    dependencies:\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /expo-font@14.0.8(expo@54.0.10)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-bTUHaJWRZ7ywP8dg3f+wfOwv6RwMV3mWT2CDUIhsK70GjNGlCtiWOCoHsA5Od/esPaVxqc37cCBvQGQRFStRlA==}\n    peerDependencies:\n      expo: '*'\n      react: '*'\n      react-native: '*'\n    dependencies:\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      fontfaceobserver: 2.3.0\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /expo-keep-awake@15.0.7(expo@54.0.10)(react@19.1.0):\n    resolution: {integrity: sha512-CgBNcWVPnrIVII5G54QDqoE125l+zmqR4HR8q+MQaCfHet+dYpS5vX5zii/RMayzGN4jPgA4XYIQ28ePKFjHoA==}\n    peerDependencies:\n      expo: '*'\n      react: '*'\n    dependencies:\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      react: 19.1.0\n    dev: false\n\n  /expo-linking@8.0.8(expo@54.0.10)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      expo-constants: 18.0.9(expo@54.0.10)(react-native@0.81.4)\n      invariant: 2.2.4\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    transitivePeerDependencies:\n      - expo\n      - supports-color\n    dev: false\n\n  /expo-modules-autolinking@3.0.13:\n    resolution: {integrity: sha512-58WnM15ESTyT2v93Rba7jplXtGvh5cFbxqUCi2uTSpBf3nndDRItLzBQaoWBzAvNUhpC2j1bye7Dn/E+GJFXmw==}\n    hasBin: true\n    dependencies:\n      '@expo/spawn-async': 1.7.2\n      chalk: 4.1.2\n      commander: 7.2.0\n      glob: 10.4.5\n      require-from-string: 2.0.2\n      resolve-from: 5.0.0\n    dev: false\n\n  /expo-modules-core@3.0.18(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-9JPnjlXEFaq/uACZ7I4wb/RkgPYCEsfG75UKMvfl7P7rkymtpRGYj8/gTL2KId8Xt1fpmIPOF57U8tKamjtjXg==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      invariant: 2.2.4\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /expo-router@6.0.8(@expo/metro-runtime@6.1.2)(@types/react@19.1.13)(expo-constants@18.0.9)(expo-linking@8.0.8)(expo@54.0.10)(react-dom@19.1.1)(react-native-gesture-handler@2.28.0)(react-native-reanimated@4.1.2)(react-native-safe-area-context@5.6.1)(react-native-screens@4.16.0)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-cx6vFvBrfPNHpNbN2ij2mF5JKE4JXyq+dJVmWNqt7JplA0aohOOKXS/KQ9vQy88HpnrcJMuYqUNHp44aWyce7g==}\n    peerDependencies:\n      '@expo/metro-runtime': ^6.1.2\n      '@react-navigation/drawer': ^7.5.0\n      '@testing-library/react-native': '>= 12.0.0'\n      expo: '*'\n      expo-constants: ^18.0.9\n      expo-linking: ^8.0.8\n      react: '*'\n      react-dom: '*'\n      react-native: '*'\n      react-native-gesture-handler: '*'\n      react-native-reanimated: '*'\n      react-native-safe-area-context: '>= 5.4.0'\n      react-native-screens: '*'\n      react-native-web: '*'\n      react-server-dom-webpack: '>= 19.0.0'\n    peerDependenciesMeta:\n      '@react-navigation/drawer':\n        optional: true\n      '@testing-library/react-native':\n        optional: true\n      react-dom:\n        optional: true\n      react-native-gesture-handler:\n        optional: true\n      react-native-reanimated:\n        optional: true\n      react-native-web:\n        optional: true\n      react-server-dom-webpack:\n        optional: true\n    dependencies:\n      '@expo/metro-runtime': 6.1.2(expo@54.0.10)(react-dom@19.1.1)(react-native@0.81.4)(react@19.1.0)\n      '@expo/schema-utils': 0.1.7\n      '@expo/server': 0.7.5\n      '@radix-ui/react-slot': 1.2.0(@types/react@19.1.13)(react@19.1.0)\n      '@radix-ui/react-tabs': 1.1.13(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      '@react-navigation/bottom-tabs': 7.4.7(@react-navigation/native@7.1.17)(react-native-safe-area-context@5.6.1)(react-native-screens@4.16.0)(react-native@0.81.4)(react@19.1.0)\n      '@react-navigation/native': 7.1.17(react-native@0.81.4)(react@19.1.0)\n      '@react-navigation/native-stack': 7.3.26(@react-navigation/native@7.1.17)(react-native-safe-area-context@5.6.1)(react-native-screens@4.16.0)(react-native@0.81.4)(react@19.1.0)\n      client-only: 0.0.1\n      debug: 4.4.3\n      escape-string-regexp: 4.0.0\n      expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0)\n      expo-constants: 18.0.9(expo@54.0.10)(react-native@0.81.4)\n      expo-linking: 8.0.8(expo@54.0.10)(react-native@0.81.4)(react@19.1.0)\n      fast-deep-equal: 3.1.3\n      invariant: 2.2.4\n      nanoid: 3.3.11\n      query-string: 7.1.3\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n      react-fast-compare: 3.2.2\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-native-gesture-handler: 2.28.0(react-native@0.81.4)(react@19.1.0)\n      react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4)(react@19.1.0)\n      react-native-reanimated: 4.1.2(@babel/core@7.28.4)(react-native-worklets@0.5.1)(react-native@0.81.4)(react@19.1.0)\n      react-native-safe-area-context: 5.6.1(react-native@0.81.4)(react@19.1.0)\n      react-native-screens: 4.16.0(react-native@0.81.4)(react@19.1.0)\n      semver: 7.6.3\n      server-only: 0.0.1\n      sf-symbols-typescript: 2.1.0\n      shallowequal: 1.1.0\n      use-latest-callback: 0.2.4(react@19.1.0)\n      vaul: 1.1.2(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n    transitivePeerDependencies:\n      - '@react-native-masked-view/masked-view'\n      - '@types/react'\n      - '@types/react-dom'\n      - supports-color\n    dev: false\n\n  /expo-status-bar@3.0.8(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4)(react@19.1.0)\n    dev: false\n\n  /expo@54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-49+IginEoKC+g125ZlRvUYNl9jKjjHcDiDnQvejNWlMQ0LtcFIWiFad/PLjmi7YqF/0rj9u3FNxqM6jNP16O0w==}\n    hasBin: true\n    peerDependencies:\n      '@expo/dom-webview': '*'\n      '@expo/metro-runtime': '*'\n      react: '*'\n      react-native: '*'\n      react-native-webview: '*'\n    peerDependenciesMeta:\n      '@expo/dom-webview':\n        optional: true\n      '@expo/metro-runtime':\n        optional: true\n      react-native-webview:\n        optional: true\n    dependencies:\n      '@babel/runtime': 7.28.4\n      '@expo/cli': 54.0.8(expo-router@6.0.8)(expo@54.0.10)(react-native@0.81.4)\n      '@expo/config': 12.0.9\n      '@expo/config-plugins': 54.0.1\n      '@expo/devtools': 0.1.7(react-native@0.81.4)(react@19.1.0)\n      '@expo/fingerprint': 0.15.1\n      '@expo/metro': 54.0.0\n      '@expo/metro-config': 54.0.5(expo@54.0.10)\n      '@expo/metro-runtime': 6.1.2(expo@54.0.10)(react-dom@19.1.1)(react-native@0.81.4)(react@19.1.0)\n      '@expo/vector-icons': 15.0.2(expo-font@14.0.8)(react-native@0.81.4)(react@19.1.0)\n      '@ungap/structured-clone': 1.3.0\n      babel-preset-expo: 54.0.3(@babel/core@7.28.4)(@babel/runtime@7.28.4)(expo@54.0.10)(react-refresh@0.14.2)\n      expo-asset: 12.0.9(expo@54.0.10)(react-native@0.81.4)(react@19.1.0)\n      expo-constants: 18.0.9(expo@54.0.10)(react-native@0.81.4)\n      expo-file-system: 19.0.15(expo@54.0.10)(react-native@0.81.4)\n      expo-font: 14.0.8(expo@54.0.10)(react-native@0.81.4)(react@19.1.0)\n      expo-keep-awake: 15.0.7(expo@54.0.10)(react@19.1.0)\n      expo-modules-autolinking: 3.0.13\n      expo-modules-core: 3.0.18(react-native@0.81.4)(react@19.1.0)\n      pretty-format: 29.7.0\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-refresh: 0.14.2\n      whatwg-url-without-unicode: 8.0.0-3\n    transitivePeerDependencies:\n      - '@babel/core'\n      - '@modelcontextprotocol/sdk'\n      - bufferutil\n      - expo-router\n      - graphql\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /exponential-backoff@3.1.2:\n    resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}\n    dev: false\n\n  /fast-deep-equal@3.1.3:\n    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}\n    dev: false\n\n  /fast-json-stable-stringify@2.1.0:\n    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}\n    dev: false\n\n  /fb-watchman@2.0.2:\n    resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}\n    dependencies:\n      bser: 2.1.1\n    dev: false\n\n  /fill-range@7.1.1:\n    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}\n    engines: {node: '>=8'}\n    dependencies:\n      to-regex-range: 5.0.1\n    dev: false\n\n  /filter-obj@1.1.0:\n    resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /finalhandler@1.1.2:\n    resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}\n    engines: {node: '>= 0.8'}\n    dependencies:\n      debug: 2.6.9\n      encodeurl: 1.0.2\n      escape-html: 1.0.3\n      on-finished: 2.3.0\n      parseurl: 1.3.3\n      statuses: 1.5.0\n      unpipe: 1.0.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /find-babel-config@2.1.2:\n    resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==}\n    dependencies:\n      json5: 2.2.3\n    dev: false\n\n  /find-up@3.0.0:\n    resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}\n    engines: {node: '>=6'}\n    dependencies:\n      locate-path: 3.0.0\n    dev: false\n\n  /find-up@4.1.0:\n    resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}\n    engines: {node: '>=8'}\n    dependencies:\n      locate-path: 5.0.0\n      path-exists: 4.0.0\n    dev: false\n\n  /find-up@5.0.0:\n    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}\n    engines: {node: '>=10'}\n    dependencies:\n      locate-path: 6.0.0\n      path-exists: 4.0.0\n    dev: false\n\n  /flow-enums-runtime@0.0.6:\n    resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}\n    dev: false\n\n  /follow-redirects@1.15.11:\n    resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}\n    engines: {node: '>=4.0'}\n    peerDependencies:\n      debug: '*'\n    peerDependenciesMeta:\n      debug:\n        optional: true\n    dev: false\n\n  /fontfaceobserver@2.3.0:\n    resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==}\n    dev: false\n\n  /foreground-child@3.3.1:\n    resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}\n    engines: {node: '>=14'}\n    dependencies:\n      cross-spawn: 7.0.6\n      signal-exit: 4.1.0\n    dev: false\n\n  /form-data@4.0.4:\n    resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}\n    engines: {node: '>= 6'}\n    dependencies:\n      asynckit: 0.4.0\n      combined-stream: 1.0.8\n      es-set-tostringtag: 2.1.0\n      hasown: 2.0.2\n      mime-types: 2.1.35\n    dev: false\n\n  /freeport-async@2.0.0:\n    resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /fresh@0.5.2:\n    resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}\n    engines: {node: '>= 0.6'}\n    dev: false\n\n  /fs.realpath@1.0.0:\n    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}\n    dev: false\n\n  /fsevents@2.3.3:\n    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}\n    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}\n    os: [darwin]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /function-bind@1.1.2:\n    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}\n    dev: false\n\n  /gensync@1.0.0-beta.2:\n    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}\n    engines: {node: '>=6.9.0'}\n    dev: false\n\n  /get-caller-file@2.0.5:\n    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}\n    engines: {node: 6.* || 8.* || >= 10.*}\n    dev: false\n\n  /get-intrinsic@1.3.0:\n    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      call-bind-apply-helpers: 1.0.2\n      es-define-property: 1.0.1\n      es-errors: 1.3.0\n      es-object-atoms: 1.1.1\n      function-bind: 1.1.2\n      get-proto: 1.0.1\n      gopd: 1.2.0\n      has-symbols: 1.1.0\n      hasown: 2.0.2\n      math-intrinsics: 1.1.0\n    dev: false\n\n  /get-nonce@1.0.1:\n    resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /get-package-type@0.1.0:\n    resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}\n    engines: {node: '>=8.0.0'}\n    dev: false\n\n  /get-proto@1.0.1:\n    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      dunder-proto: 1.0.1\n      es-object-atoms: 1.1.1\n    dev: false\n\n  /getenv@2.0.0:\n    resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /glob@10.4.5:\n    resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}\n    hasBin: true\n    dependencies:\n      foreground-child: 3.3.1\n      jackspeak: 3.4.3\n      minimatch: 9.0.5\n      minipass: 7.1.2\n      package-json-from-dist: 1.0.1\n      path-scurry: 1.11.1\n    dev: false\n\n  /glob@7.2.3:\n    resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}\n    deprecated: Glob versions prior to v9 are no longer supported\n    dependencies:\n      fs.realpath: 1.0.0\n      inflight: 1.0.6\n      inherits: 2.0.4\n      minimatch: 3.1.2\n      once: 1.4.0\n      path-is-absolute: 1.0.1\n    dev: false\n\n  /glob@9.3.5:\n    resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}\n    engines: {node: '>=16 || 14 >=14.17'}\n    dependencies:\n      fs.realpath: 1.0.0\n      minimatch: 8.0.4\n      minipass: 4.2.8\n      path-scurry: 1.11.1\n    dev: false\n\n  /global-dirs@0.1.1:\n    resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==}\n    engines: {node: '>=4'}\n    dependencies:\n      ini: 1.3.8\n    dev: false\n\n  /gopd@1.2.0:\n    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}\n    engines: {node: '>= 0.4'}\n    dev: false\n\n  /graceful-fs@4.2.11:\n    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}\n    dev: false\n\n  /has-flag@3.0.0:\n    resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /has-flag@4.0.0:\n    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /has-symbols@1.1.0:\n    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}\n    engines: {node: '>= 0.4'}\n    dev: false\n\n  /has-tostringtag@1.0.2:\n    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      has-symbols: 1.1.0\n    dev: false\n\n  /hasown@2.0.2:\n    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      function-bind: 1.1.2\n    dev: false\n\n  /hermes-estree@0.29.1:\n    resolution: {integrity: sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==}\n    dev: false\n\n  /hermes-estree@0.32.0:\n    resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==}\n    dev: false\n\n  /hermes-parser@0.29.1:\n    resolution: {integrity: sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==}\n    dependencies:\n      hermes-estree: 0.29.1\n    dev: false\n\n  /hermes-parser@0.32.0:\n    resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==}\n    dependencies:\n      hermes-estree: 0.32.0\n    dev: false\n\n  /hoist-non-react-statics@3.3.2:\n    resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}\n    dependencies:\n      react-is: 16.13.1\n    dev: false\n\n  /hosted-git-info@7.0.2:\n    resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}\n    engines: {node: ^16.14.0 || >=18.0.0}\n    dependencies:\n      lru-cache: 10.4.3\n    dev: false\n\n  /http-errors@2.0.0:\n    resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}\n    engines: {node: '>= 0.8'}\n    dependencies:\n      depd: 2.0.0\n      inherits: 2.0.4\n      setprototypeof: 1.2.0\n      statuses: 2.0.1\n      toidentifier: 1.0.1\n    dev: false\n\n  /https-proxy-agent@7.0.6:\n    resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}\n    engines: {node: '>= 14'}\n    dependencies:\n      agent-base: 7.1.4\n      debug: 4.4.3\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /ieee754@1.2.1:\n    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}\n    dev: false\n\n  /ignore@5.3.2:\n    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}\n    engines: {node: '>= 4'}\n    dev: false\n\n  /image-size@1.2.1:\n    resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}\n    engines: {node: '>=16.x'}\n    hasBin: true\n    dependencies:\n      queue: 6.0.2\n    dev: false\n\n  /import-fresh@2.0.0:\n    resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}\n    engines: {node: '>=4'}\n    dependencies:\n      caller-path: 2.0.0\n      resolve-from: 3.0.0\n    dev: false\n\n  /import-fresh@3.3.1:\n    resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}\n    engines: {node: '>=6'}\n    dependencies:\n      parent-module: 1.0.1\n      resolve-from: 4.0.0\n    dev: false\n\n  /imurmurhash@0.1.4:\n    resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}\n    engines: {node: '>=0.8.19'}\n    dev: false\n\n  /inflight@1.0.6:\n    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}\n    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.\n    dependencies:\n      once: 1.4.0\n      wrappy: 1.0.2\n    dev: false\n\n  /inherits@2.0.4:\n    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}\n    dev: false\n\n  /ini@1.3.8:\n    resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}\n    dev: false\n\n  /invariant@2.2.4:\n    resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}\n    dependencies:\n      loose-envify: 1.4.0\n    dev: false\n\n  /is-arrayish@0.2.1:\n    resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}\n    dev: false\n\n  /is-arrayish@0.3.4:\n    resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}\n    dev: false\n\n  /is-core-module@2.16.1:\n    resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}\n    engines: {node: '>= 0.4'}\n    dependencies:\n      hasown: 2.0.2\n    dev: false\n\n  /is-directory@0.3.1:\n    resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /is-docker@2.2.1:\n    resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}\n    engines: {node: '>=8'}\n    hasBin: true\n    dev: false\n\n  /is-fullwidth-code-point@3.0.0:\n    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /is-number@7.0.0:\n    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}\n    engines: {node: '>=0.12.0'}\n    dev: false\n\n  /is-plain-obj@2.1.0:\n    resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /is-what@4.1.16:\n    resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}\n    engines: {node: '>=12.13'}\n    dev: false\n\n  /is-wsl@2.2.0:\n    resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}\n    engines: {node: '>=8'}\n    dependencies:\n      is-docker: 2.2.1\n    dev: false\n\n  /isexe@2.0.0:\n    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}\n\n  /istanbul-lib-coverage@3.2.2:\n    resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /istanbul-lib-instrument@5.2.1:\n    resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}\n    engines: {node: '>=8'}\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/parser': 7.28.4\n      '@istanbuljs/schema': 0.1.3\n      istanbul-lib-coverage: 3.2.2\n      semver: 6.3.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /jackspeak@3.4.3:\n    resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}\n    dependencies:\n      '@isaacs/cliui': 8.0.2\n    optionalDependencies:\n      '@pkgjs/parseargs': 0.11.0\n    dev: false\n\n  /jest-environment-node@29.7.0:\n    resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/environment': 29.7.0\n      '@jest/fake-timers': 29.7.0\n      '@jest/types': 29.6.3\n      '@types/node': 24.5.2\n      jest-mock: 29.7.0\n      jest-util: 29.7.0\n    dev: false\n\n  /jest-get-type@29.6.3:\n    resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dev: false\n\n  /jest-haste-map@29.7.0:\n    resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/types': 29.6.3\n      '@types/graceful-fs': 4.1.9\n      '@types/node': 24.5.2\n      anymatch: 3.1.3\n      fb-watchman: 2.0.2\n      graceful-fs: 4.2.11\n      jest-regex-util: 29.6.3\n      jest-util: 29.7.0\n      jest-worker: 29.7.0\n      micromatch: 4.0.8\n      walker: 1.0.8\n    optionalDependencies:\n      fsevents: 2.3.3\n    dev: false\n\n  /jest-message-util@29.7.0:\n    resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@babel/code-frame': 7.27.1\n      '@jest/types': 29.6.3\n      '@types/stack-utils': 2.0.3\n      chalk: 4.1.2\n      graceful-fs: 4.2.11\n      micromatch: 4.0.8\n      pretty-format: 29.7.0\n      slash: 3.0.0\n      stack-utils: 2.0.6\n    dev: false\n\n  /jest-mock@29.7.0:\n    resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/types': 29.6.3\n      '@types/node': 24.5.2\n      jest-util: 29.7.0\n    dev: false\n\n  /jest-regex-util@29.6.3:\n    resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dev: false\n\n  /jest-util@29.7.0:\n    resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/types': 29.6.3\n      '@types/node': 24.5.2\n      chalk: 4.1.2\n      ci-info: 3.9.0\n      graceful-fs: 4.2.11\n      picomatch: 2.3.1\n    dev: false\n\n  /jest-validate@29.7.0:\n    resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/types': 29.6.3\n      camelcase: 6.3.0\n      chalk: 4.1.2\n      jest-get-type: 29.6.3\n      leven: 3.1.0\n      pretty-format: 29.7.0\n    dev: false\n\n  /jest-worker@29.7.0:\n    resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@types/node': 24.5.2\n      jest-util: 29.7.0\n      merge-stream: 2.0.0\n      supports-color: 8.1.1\n    dev: false\n\n  /jimp-compact@0.16.1:\n    resolution: {integrity: sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==}\n    dev: false\n\n  /js-tokens@4.0.0:\n    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}\n    dev: false\n\n  /js-yaml@3.14.1:\n    resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}\n    hasBin: true\n    dependencies:\n      argparse: 1.0.10\n      esprima: 4.0.1\n    dev: false\n\n  /js-yaml@4.1.0:\n    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}\n    hasBin: true\n    dependencies:\n      argparse: 2.0.1\n    dev: false\n\n  /jsc-safe-url@0.2.4:\n    resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==}\n    dev: false\n\n  /jsesc@3.0.2:\n    resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}\n    engines: {node: '>=6'}\n    hasBin: true\n    dev: false\n\n  /jsesc@3.1.0:\n    resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}\n    engines: {node: '>=6'}\n    hasBin: true\n    dev: false\n\n  /json-parse-better-errors@1.0.2:\n    resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}\n    dev: false\n\n  /json-parse-even-better-errors@2.3.1:\n    resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}\n    dev: false\n\n  /json5@2.2.3:\n    resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}\n    engines: {node: '>=6'}\n    hasBin: true\n    dev: false\n\n  /kleur@3.0.3:\n    resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /lan-network@0.1.7:\n    resolution: {integrity: sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==}\n    hasBin: true\n    dev: false\n\n  /leven@3.1.0:\n    resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /lighthouse-logger@1.4.2:\n    resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==}\n    dependencies:\n      debug: 2.6.9\n      marky: 1.3.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /lightningcss-darwin-arm64@1.30.1:\n    resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [arm64]\n    os: [darwin]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-darwin-x64@1.30.1:\n    resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [x64]\n    os: [darwin]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-freebsd-x64@1.30.1:\n    resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [x64]\n    os: [freebsd]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-linux-arm-gnueabihf@1.30.1:\n    resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [arm]\n    os: [linux]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-linux-arm64-gnu@1.30.1:\n    resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [arm64]\n    os: [linux]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-linux-arm64-musl@1.30.1:\n    resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [arm64]\n    os: [linux]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-linux-x64-gnu@1.30.1:\n    resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [x64]\n    os: [linux]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-linux-x64-musl@1.30.1:\n    resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [x64]\n    os: [linux]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-win32-arm64-msvc@1.30.1:\n    resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [arm64]\n    os: [win32]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss-win32-x64-msvc@1.30.1:\n    resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}\n    engines: {node: '>= 12.0.0'}\n    cpu: [x64]\n    os: [win32]\n    requiresBuild: true\n    dev: false\n    optional: true\n\n  /lightningcss@1.30.1:\n    resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}\n    engines: {node: '>= 12.0.0'}\n    dependencies:\n      detect-libc: 2.1.0\n    optionalDependencies:\n      lightningcss-darwin-arm64: 1.30.1\n      lightningcss-darwin-x64: 1.30.1\n      lightningcss-freebsd-x64: 1.30.1\n      lightningcss-linux-arm-gnueabihf: 1.30.1\n      lightningcss-linux-arm64-gnu: 1.30.1\n      lightningcss-linux-arm64-musl: 1.30.1\n      lightningcss-linux-x64-gnu: 1.30.1\n      lightningcss-linux-x64-musl: 1.30.1\n      lightningcss-win32-arm64-msvc: 1.30.1\n      lightningcss-win32-x64-msvc: 1.30.1\n    dev: false\n\n  /lines-and-columns@1.2.4:\n    resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}\n    dev: false\n\n  /locate-path@3.0.0:\n    resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}\n    engines: {node: '>=6'}\n    dependencies:\n      p-locate: 3.0.0\n      path-exists: 3.0.0\n    dev: false\n\n  /locate-path@5.0.0:\n    resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}\n    engines: {node: '>=8'}\n    dependencies:\n      p-locate: 4.1.0\n    dev: false\n\n  /locate-path@6.0.0:\n    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}\n    engines: {node: '>=10'}\n    dependencies:\n      p-locate: 5.0.0\n    dev: false\n\n  /lodash.debounce@4.0.8:\n    resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}\n    dev: false\n\n  /lodash.throttle@4.1.1:\n    resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}\n    dev: false\n\n  /log-symbols@2.2.0:\n    resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}\n    engines: {node: '>=4'}\n    dependencies:\n      chalk: 2.4.2\n    dev: false\n\n  /loose-envify@1.4.0:\n    resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}\n    hasBin: true\n    dependencies:\n      js-tokens: 4.0.0\n    dev: false\n\n  /lower-case@2.0.2:\n    resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}\n    dependencies:\n      tslib: 2.8.1\n    dev: false\n\n  /lru-cache@10.4.3:\n    resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}\n    dev: false\n\n  /lru-cache@5.1.1:\n    resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}\n    dependencies:\n      yallist: 3.1.1\n    dev: false\n\n  /makeerror@1.0.12:\n    resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}\n    dependencies:\n      tmpl: 1.0.5\n    dev: false\n\n  /marky@1.3.0:\n    resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}\n    dev: false\n\n  /math-intrinsics@1.1.0:\n    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}\n    engines: {node: '>= 0.4'}\n    dev: false\n\n  /mdn-data@2.0.14:\n    resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}\n    dev: false\n\n  /mdn-data@2.0.28:\n    resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}\n    dev: false\n\n  /mdn-data@2.0.30:\n    resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}\n    dev: false\n\n  /memoize-one@5.2.1:\n    resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}\n    dev: false\n\n  /merge-options@3.0.4:\n    resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==}\n    engines: {node: '>=10'}\n    dependencies:\n      is-plain-obj: 2.1.0\n    dev: false\n\n  /merge-stream@2.0.0:\n    resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}\n    dev: false\n\n  /metro-babel-transformer@0.83.1:\n    resolution: {integrity: sha512-r3xAD3964E8dwDBaZNSO2aIIvWXjIK80uO2xo0/pi3WI8XWT9h5SCjtGWtMtE5PRWw+t20TN0q1WMRsjvhC1rQ==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/core': 7.28.4\n      flow-enums-runtime: 0.0.6\n      hermes-parser: 0.29.1\n      nullthrows: 1.1.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-babel-transformer@0.83.2:\n    resolution: {integrity: sha512-rirY1QMFlA1uxH3ZiNauBninwTioOgwChnRdDcbB4tgRZ+bGX9DiXoh9QdpppiaVKXdJsII932OwWXGGV4+Nlw==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/core': 7.28.4\n      flow-enums-runtime: 0.0.6\n      hermes-parser: 0.32.0\n      nullthrows: 1.1.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-cache-key@0.83.1:\n    resolution: {integrity: sha512-ZUs+GD5CNeDLxx5UUWmfg26IL+Dnbryd+TLqTlZnDEgehkIa11kUSvgF92OFfJhONeXzV4rZDRGNXoo6JT+8Gg==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n    dev: false\n\n  /metro-cache-key@0.83.2:\n    resolution: {integrity: sha512-3EMG/GkGKYoTaf5RqguGLSWRqGTwO7NQ0qXKmNBjr0y6qD9s3VBXYlwB+MszGtmOKsqE9q3FPrE5Nd9Ipv7rZw==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n    dev: false\n\n  /metro-cache@0.83.1:\n    resolution: {integrity: sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      exponential-backoff: 3.1.2\n      flow-enums-runtime: 0.0.6\n      https-proxy-agent: 7.0.6\n      metro-core: 0.83.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-cache@0.83.2:\n    resolution: {integrity: sha512-Z43IodutUZeIS7OTH+yQFjc59QlFJ6s5OvM8p2AP9alr0+F8UKr8ADzFzoGKoHefZSKGa4bJx7MZJLF6GwPDHQ==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      exponential-backoff: 3.1.2\n      flow-enums-runtime: 0.0.6\n      https-proxy-agent: 7.0.6\n      metro-core: 0.83.2\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-config@0.83.1:\n    resolution: {integrity: sha512-HJhpZx3wyOkux/jeF1o7akFJzZFdbn6Zf7UQqWrvp7gqFqNulQ8Mju09raBgPmmSxKDl4LbbNeigkX0/nKY1QA==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      connect: 3.7.0\n      cosmiconfig: 5.2.1\n      flow-enums-runtime: 0.0.6\n      jest-validate: 29.7.0\n      metro: 0.83.1\n      metro-cache: 0.83.1\n      metro-core: 0.83.1\n      metro-runtime: 0.83.1\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /metro-config@0.83.2:\n    resolution: {integrity: sha512-1FjCcdBe3e3D08gSSiU9u3Vtxd7alGH3x/DNFqWDFf5NouX4kLgbVloDDClr1UrLz62c0fHh2Vfr9ecmrOZp+g==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      connect: 3.7.0\n      flow-enums-runtime: 0.0.6\n      jest-validate: 29.7.0\n      metro: 0.83.2\n      metro-cache: 0.83.2\n      metro-core: 0.83.2\n      metro-runtime: 0.83.2\n      yaml: 2.8.1\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /metro-core@0.83.1:\n    resolution: {integrity: sha512-uVL1eAJcMFd2o2Q7dsbpg8COaxjZBBGaXqO2OHnivpCdfanraVL8dPmY6It9ZeqWLOihUKZ2yHW4b6soVCzH/Q==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n      lodash.throttle: 4.1.1\n      metro-resolver: 0.83.1\n    dev: false\n\n  /metro-core@0.83.2:\n    resolution: {integrity: sha512-8DRb0O82Br0IW77cNgKMLYWUkx48lWxUkvNUxVISyMkcNwE/9ywf1MYQUE88HaKwSrqne6kFgCSA/UWZoUT0Iw==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n      lodash.throttle: 4.1.1\n      metro-resolver: 0.83.2\n    dev: false\n\n  /metro-file-map@0.83.1:\n    resolution: {integrity: sha512-Yu429lnexKl44PttKw3nhqgmpBR+6UQ/tRaYcxPeEShtcza9DWakCn7cjqDTQZtWR2A8xSNv139izJMyQ4CG+w==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      debug: 4.4.3\n      fb-watchman: 2.0.2\n      flow-enums-runtime: 0.0.6\n      graceful-fs: 4.2.11\n      invariant: 2.2.4\n      jest-worker: 29.7.0\n      micromatch: 4.0.8\n      nullthrows: 1.1.1\n      walker: 1.0.8\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-file-map@0.83.2:\n    resolution: {integrity: sha512-cMSWnEqZrp/dzZIEd7DEDdk72PXz6w5NOKriJoDN9p1TDQ5nAYrY2lHi8d6mwbcGLoSlWmpPyny9HZYFfPWcGQ==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      debug: 4.4.3\n      fb-watchman: 2.0.2\n      flow-enums-runtime: 0.0.6\n      graceful-fs: 4.2.11\n      invariant: 2.2.4\n      jest-worker: 29.7.0\n      micromatch: 4.0.8\n      nullthrows: 1.1.1\n      walker: 1.0.8\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-minify-terser@0.83.1:\n    resolution: {integrity: sha512-kmooOxXLvKVxkh80IVSYO4weBdJDhCpg5NSPkjzzAnPJP43u6+usGXobkTWxxrAlq900bhzqKek4pBsUchlX6A==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n      terser: 5.44.0\n    dev: false\n\n  /metro-minify-terser@0.83.2:\n    resolution: {integrity: sha512-zvIxnh7U0JQ7vT4quasKsijId3dOAWgq+ip2jF/8TMrPUqQabGrs04L2dd0haQJ+PA+d4VvK/bPOY8X/vL2PWw==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n      terser: 5.44.0\n    dev: false\n\n  /metro-resolver@0.83.1:\n    resolution: {integrity: sha512-t8j46kiILAqqFS5RNa+xpQyVjULxRxlvMidqUswPEk5nQVNdlJslqizDm/Et3v/JKwOtQGkYAQCHxP1zGStR/g==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n    dev: false\n\n  /metro-resolver@0.83.2:\n    resolution: {integrity: sha512-Yf5mjyuiRE/Y+KvqfsZxrbHDA15NZxyfg8pIk0qg47LfAJhpMVEX+36e6ZRBq7KVBqy6VDX5Sq55iHGM4xSm7Q==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n    dev: false\n\n  /metro-runtime@0.83.1:\n    resolution: {integrity: sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/runtime': 7.28.4\n      flow-enums-runtime: 0.0.6\n    dev: false\n\n  /metro-runtime@0.83.2:\n    resolution: {integrity: sha512-nnsPtgRvFbNKwemqs0FuyFDzXLl+ezuFsUXDbX8o0SXOfsOPijqiQrf3kuafO1Zx1aUWf4NOrKJMAQP5EEHg9A==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/runtime': 7.28.4\n      flow-enums-runtime: 0.0.6\n    dev: false\n\n  /metro-source-map@0.83.1:\n    resolution: {integrity: sha512-De7Vbeo96fFZ2cqmI0fWwVJbtHIwPZv++LYlWSwzTiCzxBDJORncN0LcT48Vi2UlQLzXJg+/CuTAcy7NBVh69A==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/traverse': 7.28.4\n      '@babel/traverse--for-generate-function-map': /@babel/traverse@7.28.4\n      '@babel/types': 7.28.4\n      flow-enums-runtime: 0.0.6\n      invariant: 2.2.4\n      metro-symbolicate: 0.83.1\n      nullthrows: 1.1.1\n      ob1: 0.83.1\n      source-map: 0.5.7\n      vlq: 1.0.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-source-map@0.83.2:\n    resolution: {integrity: sha512-5FL/6BSQvshIKjXOennt9upFngq2lFvDakZn5LfauIVq8+L4sxXewIlSTcxAtzbtjAIaXeOSVMtCJ5DdfCt9AA==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/traverse': 7.28.4\n      '@babel/traverse--for-generate-function-map': /@babel/traverse@7.28.4\n      '@babel/types': 7.28.4\n      flow-enums-runtime: 0.0.6\n      invariant: 2.2.4\n      metro-symbolicate: 0.83.2\n      nullthrows: 1.1.1\n      ob1: 0.83.2\n      source-map: 0.5.7\n      vlq: 1.0.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-symbolicate@0.83.1:\n    resolution: {integrity: sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg==}\n    engines: {node: '>=20.19.4'}\n    hasBin: true\n    dependencies:\n      flow-enums-runtime: 0.0.6\n      invariant: 2.2.4\n      metro-source-map: 0.83.1\n      nullthrows: 1.1.1\n      source-map: 0.5.7\n      vlq: 1.0.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-symbolicate@0.83.2:\n    resolution: {integrity: sha512-KoU9BLwxxED6n33KYuQQuc5bXkIxF3fSwlc3ouxrrdLWwhu64muYZNQrukkWzhVKRNFIXW7X2iM8JXpi2heIPw==}\n    engines: {node: '>=20.19.4'}\n    hasBin: true\n    dependencies:\n      flow-enums-runtime: 0.0.6\n      invariant: 2.2.4\n      metro-source-map: 0.83.2\n      nullthrows: 1.1.1\n      source-map: 0.5.7\n      vlq: 1.0.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-transform-plugins@0.83.1:\n    resolution: {integrity: sha512-1Y+I8oozXwhuS0qwC+ezaHXBf0jXW4oeYn4X39XWbZt9X2HfjodqY9bH9r6RUTsoiK7S4j8Ni2C91bUC+sktJQ==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/generator': 7.28.3\n      '@babel/template': 7.27.2\n      '@babel/traverse': 7.28.4\n      flow-enums-runtime: 0.0.6\n      nullthrows: 1.1.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-transform-plugins@0.83.2:\n    resolution: {integrity: sha512-5WlW25WKPkiJk2yA9d8bMuZrgW7vfA4f4MBb9ZeHbTB3eIAoNN8vS8NENgG/X/90vpTB06X66OBvxhT3nHwP6A==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/generator': 7.28.3\n      '@babel/template': 7.27.2\n      '@babel/traverse': 7.28.4\n      flow-enums-runtime: 0.0.6\n      nullthrows: 1.1.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /metro-transform-worker@0.83.1:\n    resolution: {integrity: sha512-owCrhPyUxdLgXEEEAL2b14GWTPZ2zYuab1VQXcfEy0sJE71iciD7fuMcrngoufh7e7UHDZ56q4ktXg8wgiYA1Q==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/generator': 7.28.3\n      '@babel/parser': 7.28.4\n      '@babel/types': 7.28.4\n      flow-enums-runtime: 0.0.6\n      metro: 0.83.1\n      metro-babel-transformer: 0.83.1\n      metro-cache: 0.83.1\n      metro-cache-key: 0.83.1\n      metro-minify-terser: 0.83.1\n      metro-source-map: 0.83.1\n      metro-transform-plugins: 0.83.1\n      nullthrows: 1.1.1\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /metro-transform-worker@0.83.2:\n    resolution: {integrity: sha512-G5DsIg+cMZ2KNfrdLnWMvtppb3+Rp1GMyj7Bvd9GgYc/8gRmvq1XVEF9XuO87Shhb03kFhGqMTgZerz3hZ1v4Q==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/generator': 7.28.3\n      '@babel/parser': 7.28.4\n      '@babel/types': 7.28.4\n      flow-enums-runtime: 0.0.6\n      metro: 0.83.2\n      metro-babel-transformer: 0.83.2\n      metro-cache: 0.83.2\n      metro-cache-key: 0.83.2\n      metro-minify-terser: 0.83.2\n      metro-source-map: 0.83.2\n      metro-transform-plugins: 0.83.2\n      nullthrows: 1.1.1\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /metro@0.83.1:\n    resolution: {integrity: sha512-UGKepmTxoGD4HkQV8YWvpvwef7fUujNtTgG4Ygf7m/M0qjvb9VuDmAsEU+UdriRX7F61pnVK/opz89hjKlYTXA==}\n    engines: {node: '>=20.19.4'}\n    hasBin: true\n    dependencies:\n      '@babel/code-frame': 7.27.1\n      '@babel/core': 7.28.4\n      '@babel/generator': 7.28.3\n      '@babel/parser': 7.28.4\n      '@babel/template': 7.27.2\n      '@babel/traverse': 7.28.4\n      '@babel/types': 7.28.4\n      accepts: 1.3.8\n      chalk: 4.1.2\n      ci-info: 2.0.0\n      connect: 3.7.0\n      debug: 4.4.3\n      error-stack-parser: 2.1.4\n      flow-enums-runtime: 0.0.6\n      graceful-fs: 4.2.11\n      hermes-parser: 0.29.1\n      image-size: 1.2.1\n      invariant: 2.2.4\n      jest-worker: 29.7.0\n      jsc-safe-url: 0.2.4\n      lodash.throttle: 4.1.1\n      metro-babel-transformer: 0.83.1\n      metro-cache: 0.83.1\n      metro-cache-key: 0.83.1\n      metro-config: 0.83.1\n      metro-core: 0.83.1\n      metro-file-map: 0.83.1\n      metro-resolver: 0.83.1\n      metro-runtime: 0.83.1\n      metro-source-map: 0.83.1\n      metro-symbolicate: 0.83.1\n      metro-transform-plugins: 0.83.1\n      metro-transform-worker: 0.83.1\n      mime-types: 2.1.35\n      nullthrows: 1.1.1\n      serialize-error: 2.1.0\n      source-map: 0.5.7\n      throat: 5.0.0\n      ws: 7.5.10\n      yargs: 17.7.2\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /metro@0.83.2:\n    resolution: {integrity: sha512-HQgs9H1FyVbRptNSMy/ImchTTE5vS2MSqLoOo7hbDoBq6hPPZokwJvBMwrYSxdjQZmLXz2JFZtdvS+ZfgTc9yw==}\n    engines: {node: '>=20.19.4'}\n    hasBin: true\n    dependencies:\n      '@babel/code-frame': 7.27.1\n      '@babel/core': 7.28.4\n      '@babel/generator': 7.28.3\n      '@babel/parser': 7.28.4\n      '@babel/template': 7.27.2\n      '@babel/traverse': 7.28.4\n      '@babel/types': 7.28.4\n      accepts: 1.3.8\n      chalk: 4.1.2\n      ci-info: 2.0.0\n      connect: 3.7.0\n      debug: 4.4.3\n      error-stack-parser: 2.1.4\n      flow-enums-runtime: 0.0.6\n      graceful-fs: 4.2.11\n      hermes-parser: 0.32.0\n      image-size: 1.2.1\n      invariant: 2.2.4\n      jest-worker: 29.7.0\n      jsc-safe-url: 0.2.4\n      lodash.throttle: 4.1.1\n      metro-babel-transformer: 0.83.2\n      metro-cache: 0.83.2\n      metro-cache-key: 0.83.2\n      metro-config: 0.83.2\n      metro-core: 0.83.2\n      metro-file-map: 0.83.2\n      metro-resolver: 0.83.2\n      metro-runtime: 0.83.2\n      metro-source-map: 0.83.2\n      metro-symbolicate: 0.83.2\n      metro-transform-plugins: 0.83.2\n      metro-transform-worker: 0.83.2\n      mime-types: 2.1.35\n      nullthrows: 1.1.1\n      serialize-error: 2.1.0\n      source-map: 0.5.7\n      throat: 5.0.0\n      ws: 7.5.10\n      yargs: 17.7.2\n    transitivePeerDependencies:\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /micromatch@4.0.8:\n    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}\n    engines: {node: '>=8.6'}\n    dependencies:\n      braces: 3.0.3\n      picomatch: 2.3.1\n    dev: false\n\n  /mime-db@1.52.0:\n    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}\n    engines: {node: '>= 0.6'}\n    dev: false\n\n  /mime-db@1.54.0:\n    resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}\n    engines: {node: '>= 0.6'}\n    dev: false\n\n  /mime-types@2.1.35:\n    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}\n    engines: {node: '>= 0.6'}\n    dependencies:\n      mime-db: 1.52.0\n    dev: false\n\n  /mime@1.6.0:\n    resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}\n    engines: {node: '>=4'}\n    hasBin: true\n    dev: false\n\n  /mimic-fn@1.2.0:\n    resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /minimatch@3.1.2:\n    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}\n    dependencies:\n      brace-expansion: 1.1.12\n    dev: false\n\n  /minimatch@8.0.4:\n    resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==}\n    engines: {node: '>=16 || 14 >=14.17'}\n    dependencies:\n      brace-expansion: 2.0.2\n    dev: false\n\n  /minimatch@9.0.5:\n    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}\n    engines: {node: '>=16 || 14 >=14.17'}\n    dependencies:\n      brace-expansion: 2.0.2\n    dev: false\n\n  /minimist@1.2.8:\n    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}\n    dev: false\n\n  /minipass@4.2.8:\n    resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /minipass@7.1.2:\n    resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}\n    engines: {node: '>=16 || 14 >=14.17'}\n    dev: false\n\n  /minizlib@3.0.2:\n    resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}\n    engines: {node: '>= 18'}\n    dependencies:\n      minipass: 7.1.2\n    dev: false\n\n  /mkdirp@1.0.4:\n    resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}\n    engines: {node: '>=10'}\n    hasBin: true\n    dev: false\n\n  /mkdirp@3.0.1:\n    resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}\n    engines: {node: '>=10'}\n    hasBin: true\n    dev: false\n\n  /ms@2.0.0:\n    resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}\n    dev: false\n\n  /ms@2.1.3:\n    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}\n    dev: false\n\n  /mz@2.7.0:\n    resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}\n    dependencies:\n      any-promise: 1.3.0\n      object-assign: 4.1.1\n      thenify-all: 1.6.0\n    dev: false\n\n  /nanoid@3.3.11:\n    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}\n    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}\n    hasBin: true\n    dev: false\n\n  /negotiator@0.6.3:\n    resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}\n    engines: {node: '>= 0.6'}\n    dev: false\n\n  /negotiator@0.6.4:\n    resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}\n    engines: {node: '>= 0.6'}\n    dev: false\n\n  /nested-error-stacks@2.0.1:\n    resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}\n    dev: false\n\n  /no-case@3.0.4:\n    resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}\n    dependencies:\n      lower-case: 2.0.2\n      tslib: 2.8.1\n    dev: false\n\n  /node-forge@1.3.1:\n    resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}\n    engines: {node: '>= 6.13.0'}\n    dev: false\n\n  /node-int64@0.4.0:\n    resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}\n    dev: false\n\n  /node-releases@2.0.21:\n    resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}\n    dev: false\n\n  /normalize-path@3.0.0:\n    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /npm-package-arg@11.0.3:\n    resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==}\n    engines: {node: ^16.14.0 || >=18.0.0}\n    dependencies:\n      hosted-git-info: 7.0.2\n      proc-log: 4.2.0\n      semver: 7.7.2\n      validate-npm-package-name: 5.0.1\n    dev: false\n\n  /nth-check@2.1.1:\n    resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}\n    dependencies:\n      boolbase: 1.0.0\n    dev: false\n\n  /nullthrows@1.1.1:\n    resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}\n    dev: false\n\n  /ob1@0.83.1:\n    resolution: {integrity: sha512-ngwqewtdUzFyycomdbdIhFLjePPSOt1awKMUXQ0L7iLHgWEPF3DsCerblzjzfAUHaXuvE9ccJymWQ/4PNNqvnQ==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n    dev: false\n\n  /ob1@0.83.2:\n    resolution: {integrity: sha512-XlK3w4M+dwd1g1gvHzVbxiXEbUllRONEgcF2uEO0zm4nxa0eKlh41c6N65q1xbiDOeKKda1tvNOAD33fNjyvCg==}\n    engines: {node: '>=20.19.4'}\n    dependencies:\n      flow-enums-runtime: 0.0.6\n    dev: false\n\n  /object-assign@4.1.1:\n    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /on-finished@2.3.0:\n    resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}\n    engines: {node: '>= 0.8'}\n    dependencies:\n      ee-first: 1.1.1\n    dev: false\n\n  /on-finished@2.4.1:\n    resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}\n    engines: {node: '>= 0.8'}\n    dependencies:\n      ee-first: 1.1.1\n    dev: false\n\n  /on-headers@1.1.0:\n    resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /once@1.4.0:\n    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}\n    dependencies:\n      wrappy: 1.0.2\n    dev: false\n\n  /onetime@2.0.1:\n    resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==}\n    engines: {node: '>=4'}\n    dependencies:\n      mimic-fn: 1.2.0\n    dev: false\n\n  /open@7.4.2:\n    resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}\n    engines: {node: '>=8'}\n    dependencies:\n      is-docker: 2.2.1\n      is-wsl: 2.2.0\n    dev: false\n\n  /open@8.4.2:\n    resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}\n    engines: {node: '>=12'}\n    dependencies:\n      define-lazy-prop: 2.0.0\n      is-docker: 2.2.1\n      is-wsl: 2.2.0\n    dev: false\n\n  /ora@3.4.0:\n    resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==}\n    engines: {node: '>=6'}\n    dependencies:\n      chalk: 2.4.2\n      cli-cursor: 2.1.0\n      cli-spinners: 2.9.2\n      log-symbols: 2.2.0\n      strip-ansi: 5.2.0\n      wcwidth: 1.0.1\n    dev: false\n\n  /p-limit@2.3.0:\n    resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}\n    engines: {node: '>=6'}\n    dependencies:\n      p-try: 2.2.0\n    dev: false\n\n  /p-limit@3.1.0:\n    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}\n    engines: {node: '>=10'}\n    dependencies:\n      yocto-queue: 0.1.0\n    dev: false\n\n  /p-locate@3.0.0:\n    resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}\n    engines: {node: '>=6'}\n    dependencies:\n      p-limit: 2.3.0\n    dev: false\n\n  /p-locate@4.1.0:\n    resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}\n    engines: {node: '>=8'}\n    dependencies:\n      p-limit: 2.3.0\n    dev: false\n\n  /p-locate@5.0.0:\n    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}\n    engines: {node: '>=10'}\n    dependencies:\n      p-limit: 3.1.0\n    dev: false\n\n  /p-try@2.2.0:\n    resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /package-json-from-dist@1.0.1:\n    resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}\n    dev: false\n\n  /parent-module@1.0.1:\n    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}\n    engines: {node: '>=6'}\n    dependencies:\n      callsites: 3.1.0\n    dev: false\n\n  /parse-json@4.0.0:\n    resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}\n    engines: {node: '>=4'}\n    dependencies:\n      error-ex: 1.3.4\n      json-parse-better-errors: 1.0.2\n    dev: false\n\n  /parse-json@5.2.0:\n    resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}\n    engines: {node: '>=8'}\n    dependencies:\n      '@babel/code-frame': 7.27.1\n      error-ex: 1.3.4\n      json-parse-even-better-errors: 2.3.1\n      lines-and-columns: 1.2.4\n    dev: false\n\n  /parse-png@2.1.0:\n    resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==}\n    engines: {node: '>=10'}\n    dependencies:\n      pngjs: 3.4.0\n    dev: false\n\n  /parseurl@1.3.3:\n    resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /path-dirname@1.0.2:\n    resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}\n    dev: false\n\n  /path-exists@3.0.0:\n    resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /path-exists@4.0.0:\n    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /path-is-absolute@1.0.1:\n    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /path-key@3.1.1:\n    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}\n    engines: {node: '>=8'}\n\n  /path-parse@1.0.7:\n    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}\n    dev: false\n\n  /path-scurry@1.11.1:\n    resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}\n    engines: {node: '>=16 || 14 >=14.18'}\n    dependencies:\n      lru-cache: 10.4.3\n      minipass: 7.1.2\n    dev: false\n\n  /path-type@4.0.0:\n    resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /picocolors@1.1.1:\n    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}\n    dev: false\n\n  /picomatch@2.3.1:\n    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}\n    engines: {node: '>=8.6'}\n    dev: false\n\n  /picomatch@3.0.1:\n    resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /pirates@4.0.7:\n    resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}\n    engines: {node: '>= 6'}\n    dev: false\n\n  /pkg-up@3.1.0:\n    resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}\n    engines: {node: '>=8'}\n    dependencies:\n      find-up: 3.0.0\n    dev: false\n\n  /plist@3.1.0:\n    resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}\n    engines: {node: '>=10.4.0'}\n    dependencies:\n      '@xmldom/xmldom': 0.8.11\n      base64-js: 1.5.1\n      xmlbuilder: 15.1.1\n    dev: false\n\n  /pngjs@3.4.0:\n    resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}\n    engines: {node: '>=4.0.0'}\n    dev: false\n\n  /postcss@8.4.49:\n    resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}\n    engines: {node: ^10 || ^12 || >=14}\n    dependencies:\n      nanoid: 3.3.11\n      picocolors: 1.1.1\n      source-map-js: 1.2.1\n    dev: false\n\n  /pretty-bytes@5.6.0:\n    resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /pretty-format@29.7.0:\n    resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}\n    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}\n    dependencies:\n      '@jest/schemas': 29.6.3\n      ansi-styles: 5.2.0\n      react-is: 18.3.1\n    dev: false\n\n  /proc-log@4.2.0:\n    resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==}\n    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}\n    dev: false\n\n  /progress@2.0.3:\n    resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}\n    engines: {node: '>=0.4.0'}\n    dev: false\n\n  /promise@8.3.0:\n    resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==}\n    dependencies:\n      asap: 2.0.6\n    dev: false\n\n  /prompts@2.4.2:\n    resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}\n    engines: {node: '>= 6'}\n    dependencies:\n      kleur: 3.0.3\n      sisteransi: 1.0.5\n    dev: false\n\n  /proxy-from-env@1.1.0:\n    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}\n    dev: false\n\n  /punycode@2.3.1:\n    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /qrcode-terminal@0.11.0:\n    resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==}\n    hasBin: true\n    dev: false\n\n  /query-string@7.1.3:\n    resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}\n    engines: {node: '>=6'}\n    dependencies:\n      decode-uri-component: 0.2.2\n      filter-obj: 1.1.0\n      split-on-first: 1.1.0\n      strict-uri-encode: 2.0.0\n    dev: false\n\n  /queue@6.0.2:\n    resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}\n    dependencies:\n      inherits: 2.0.4\n    dev: false\n\n  /range-parser@1.2.1:\n    resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}\n    engines: {node: '>= 0.6'}\n    dev: false\n\n  /rc@1.2.8:\n    resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}\n    hasBin: true\n    dependencies:\n      deep-extend: 0.6.0\n      ini: 1.3.8\n      minimist: 1.2.8\n      strip-json-comments: 2.0.1\n    dev: false\n\n  /react-devtools-core@6.1.5:\n    resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==}\n    dependencies:\n      shell-quote: 1.8.3\n      ws: 7.5.10\n    transitivePeerDependencies:\n      - bufferutil\n      - utf-8-validate\n    dev: false\n\n  /react-dom@19.1.1(react@19.1.0):\n    resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}\n    peerDependencies:\n      react: ^19.1.1\n    dependencies:\n      react: 19.1.0\n      scheduler: 0.26.0\n    dev: false\n\n  /react-fast-compare@3.2.2:\n    resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}\n    dev: false\n\n  /react-freeze@1.0.4(react@19.1.0):\n    resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==}\n    engines: {node: '>=10'}\n    peerDependencies:\n      react: '>=17.0.0'\n    dependencies:\n      react: 19.1.0\n    dev: false\n\n  /react-is@16.13.1:\n    resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}\n    dev: false\n\n  /react-is@18.3.1:\n    resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}\n    dev: false\n\n  /react-is@19.1.1:\n    resolution: {integrity: sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==}\n    dev: false\n\n  /react-native-gesture-handler@2.28.0(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      '@egjs/hammerjs': 2.0.17\n      hoist-non-react-statics: 3.3.2\n      invariant: 2.2.4\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /react-native-is-edge-to-edge@1.2.1(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /react-native-reanimated@4.1.2(@babel/core@7.28.4)(react-native-worklets@0.5.1)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-qzmQiFrvjm62pRBcj97QI9Xckc3EjgHQoY1F2yjktd0kpjhoyePeuTEXjYRCAVIy7IV/1cfeSup34+zFThFoHQ==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n      react: '*'\n      react-native: '*'\n      react-native-worklets: '>=0.5.0'\n    dependencies:\n      '@babel/core': 7.28.4\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4)(react@19.1.0)\n      react-native-worklets: 0.5.1(@babel/core@7.28.4)(react-native@0.81.4)(react@19.1.0)\n      semver: 7.7.2\n    dev: false\n\n  /react-native-safe-area-context@5.6.1(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /react-native-screens@4.16.0(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      react: 19.1.0\n      react-freeze: 1.0.4(react@19.1.0)\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4)(react@19.1.0)\n      warn-once: 0.1.1\n    dev: false\n\n  /react-native-svg-transformer@1.5.1(react-native-svg@15.12.1)(react-native@0.81.4)(typescript@5.9.2):\n    resolution: {integrity: sha512-dFvBNR8A9VPum9KCfh+LE49YiJEF8zUSnEFciKQroR/bEOhlPoZA0SuQ0qNk7m2iZl2w59FYjdRe0pMHWMDl0Q==}\n    peerDependencies:\n      react-native: '>=0.59.0'\n      react-native-svg: '>=12.0.0'\n    dependencies:\n      '@svgr/core': 8.1.0(typescript@5.9.2)\n      '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0)\n      '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0)(typescript@5.9.2)\n      path-dirname: 1.0.2\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      react-native-svg: 15.12.1(react-native@0.81.4)(react@19.1.0)\n    transitivePeerDependencies:\n      - supports-color\n      - typescript\n    dev: false\n\n  /react-native-svg@15.12.1(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==}\n    peerDependencies:\n      react: '*'\n      react-native: '*'\n    dependencies:\n      css-select: 5.2.2\n      css-tree: 1.1.3\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      warn-once: 0.1.1\n    dev: false\n\n  /react-native-worklets@0.5.1(@babel/core@7.28.4)(react-native@0.81.4)(react@19.1.0):\n    resolution: {integrity: sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==}\n    peerDependencies:\n      '@babel/core': 7.28.4\n      react: '*'\n      react-native: '*'\n    dependencies:\n      '@babel/core': 7.28.4\n      '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.4)\n      '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.4)\n      '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.4)\n      '@babel/preset-typescript': 7.27.1(@babel/core@7.28.4)\n      convert-source-map: 2.0.0\n      react: 19.1.0\n      react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0)\n      semver: 7.7.2\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==}\n    engines: {node: '>= 20.19.4'}\n    hasBin: true\n    peerDependencies:\n      '@types/react': ^19.1.0\n      react: ^19.1.0\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@jest/create-cache-key-function': 29.7.0\n      '@react-native/assets-registry': 0.81.4\n      '@react-native/codegen': 0.81.4(@babel/core@7.28.4)\n      '@react-native/community-cli-plugin': 0.81.4\n      '@react-native/gradle-plugin': 0.81.4\n      '@react-native/js-polyfills': 0.81.4\n      '@react-native/normalize-colors': 0.81.4\n      '@react-native/virtualized-lists': 0.81.4(@types/react@19.1.13)(react-native@0.81.4)(react@19.1.0)\n      '@types/react': 19.1.13\n      abort-controller: 3.0.0\n      anser: 1.4.10\n      ansi-regex: 5.0.1\n      babel-jest: 29.7.0(@babel/core@7.28.4)\n      babel-plugin-syntax-hermes-parser: 0.29.1\n      base64-js: 1.5.1\n      commander: 12.1.0\n      flow-enums-runtime: 0.0.6\n      glob: 7.2.3\n      invariant: 2.2.4\n      jest-environment-node: 29.7.0\n      memoize-one: 5.2.1\n      metro-runtime: 0.83.2\n      metro-source-map: 0.83.2\n      nullthrows: 1.1.1\n      pretty-format: 29.7.0\n      promise: 8.3.0\n      react: 19.1.0\n      react-devtools-core: 6.1.5\n      react-refresh: 0.14.2\n      regenerator-runtime: 0.13.11\n      scheduler: 0.26.0\n      semver: 7.7.2\n      stacktrace-parser: 0.1.11\n      whatwg-fetch: 3.6.20\n      ws: 6.2.3\n      yargs: 17.7.2\n    transitivePeerDependencies:\n      - '@babel/core'\n      - '@react-native-community/cli'\n      - '@react-native/metro-config'\n      - bufferutil\n      - supports-color\n      - utf-8-validate\n    dev: false\n\n  /react-refresh@0.14.2:\n    resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /react-remove-scroll-bar@2.3.8(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}\n    engines: {node: '>=10'}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-style-singleton: 2.2.3(@types/react@19.1.13)(react@19.1.0)\n      tslib: 2.8.1\n    dev: false\n\n  /react-remove-scroll@2.7.1(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==}\n    engines: {node: '>=10'}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n      react-remove-scroll-bar: 2.3.8(@types/react@19.1.13)(react@19.1.0)\n      react-style-singleton: 2.2.3(@types/react@19.1.13)(react@19.1.0)\n      tslib: 2.8.1\n      use-callback-ref: 1.3.3(@types/react@19.1.13)(react@19.1.0)\n      use-sidecar: 1.1.3(@types/react@19.1.13)(react@19.1.0)\n    dev: false\n\n  /react-style-singleton@2.2.3(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}\n    engines: {node: '>=10'}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      get-nonce: 1.0.1\n      react: 19.1.0\n      tslib: 2.8.1\n    dev: false\n\n  /react@19.1.0:\n    resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /regenerate-unicode-properties@10.2.2:\n    resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}\n    engines: {node: '>=4'}\n    dependencies:\n      regenerate: 1.4.2\n    dev: false\n\n  /regenerate@1.4.2:\n    resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}\n    dev: false\n\n  /regenerator-runtime@0.13.11:\n    resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}\n    dev: false\n\n  /regexpu-core@6.3.1:\n    resolution: {integrity: sha512-DzcswPr252wEr7Qz8AyAVbfyBDKLoYp6eRA1We2Fa9qirRFSdtkP5sHr3yglDKy2BbA0fd2T+j/CUSKes3FeVQ==}\n    engines: {node: '>=4'}\n    dependencies:\n      regenerate: 1.4.2\n      regenerate-unicode-properties: 10.2.2\n      regjsgen: 0.8.0\n      regjsparser: 0.12.0\n      unicode-match-property-ecmascript: 2.0.0\n      unicode-match-property-value-ecmascript: 2.2.1\n    dev: false\n\n  /regjsgen@0.8.0:\n    resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==}\n    dev: false\n\n  /regjsparser@0.12.0:\n    resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}\n    hasBin: true\n    dependencies:\n      jsesc: 3.0.2\n    dev: false\n\n  /require-directory@2.1.1:\n    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /require-from-string@2.0.2:\n    resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /requireg@0.2.2:\n    resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==}\n    engines: {node: '>= 4.0.0'}\n    dependencies:\n      nested-error-stacks: 2.0.1\n      rc: 1.2.8\n      resolve: 1.7.1\n    dev: false\n\n  /reselect@4.1.8:\n    resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}\n    dev: false\n\n  /resolve-from@3.0.0:\n    resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /resolve-from@4.0.0:\n    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /resolve-from@5.0.0:\n    resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /resolve-global@1.0.0:\n    resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==}\n    engines: {node: '>=8'}\n    dependencies:\n      global-dirs: 0.1.1\n    dev: false\n\n  /resolve-workspace-root@2.0.0:\n    resolution: {integrity: sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw==}\n    dev: false\n\n  /resolve.exports@2.0.3:\n    resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /resolve@1.22.10:\n    resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}\n    engines: {node: '>= 0.4'}\n    hasBin: true\n    dependencies:\n      is-core-module: 2.16.1\n      path-parse: 1.0.7\n      supports-preserve-symlinks-flag: 1.0.0\n    dev: false\n\n  /resolve@1.7.1:\n    resolution: {integrity: sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==}\n    dependencies:\n      path-parse: 1.0.7\n    dev: false\n\n  /restore-cursor@2.0.0:\n    resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}\n    engines: {node: '>=4'}\n    dependencies:\n      onetime: 2.0.1\n      signal-exit: 3.0.7\n    dev: false\n\n  /rimraf@3.0.2:\n    resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}\n    deprecated: Rimraf versions prior to v4 are no longer supported\n    hasBin: true\n    dependencies:\n      glob: 7.2.3\n    dev: false\n\n  /safe-buffer@5.2.1:\n    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}\n    dev: false\n\n  /sax@1.4.1:\n    resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}\n    dev: false\n\n  /scheduler@0.26.0:\n    resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}\n    dev: false\n\n  /semver@6.3.1:\n    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}\n    hasBin: true\n    dev: false\n\n  /semver@7.6.3:\n    resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}\n    engines: {node: '>=10'}\n    hasBin: true\n    dev: false\n\n  /semver@7.7.2:\n    resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}\n    engines: {node: '>=10'}\n    hasBin: true\n    dev: false\n\n  /send@0.19.0:\n    resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}\n    engines: {node: '>= 0.8.0'}\n    dependencies:\n      debug: 2.6.9\n      depd: 2.0.0\n      destroy: 1.2.0\n      encodeurl: 1.0.2\n      escape-html: 1.0.3\n      etag: 1.8.1\n      fresh: 0.5.2\n      http-errors: 2.0.0\n      mime: 1.6.0\n      ms: 2.1.3\n      on-finished: 2.4.1\n      range-parser: 1.2.1\n      statuses: 2.0.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /send@0.19.1:\n    resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==}\n    engines: {node: '>= 0.8.0'}\n    dependencies:\n      debug: 2.6.9\n      depd: 2.0.0\n      destroy: 1.2.0\n      encodeurl: 2.0.0\n      escape-html: 1.0.3\n      etag: 1.8.1\n      fresh: 0.5.2\n      http-errors: 2.0.0\n      mime: 1.6.0\n      ms: 2.1.3\n      on-finished: 2.4.1\n      range-parser: 1.2.1\n      statuses: 2.0.1\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /serialize-error@2.1.0:\n    resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /serve-static@1.16.2:\n    resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}\n    engines: {node: '>= 0.8.0'}\n    dependencies:\n      encodeurl: 2.0.0\n      escape-html: 1.0.3\n      parseurl: 1.3.3\n      send: 0.19.0\n    transitivePeerDependencies:\n      - supports-color\n    dev: false\n\n  /server-only@0.0.1:\n    resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}\n    dev: false\n\n  /setprototypeof@1.2.0:\n    resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}\n    dev: false\n\n  /sf-symbols-typescript@2.1.0:\n    resolution: {integrity: sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /shallowequal@1.1.0:\n    resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}\n    dev: false\n\n  /shebang-command@2.0.0:\n    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}\n    engines: {node: '>=8'}\n    dependencies:\n      shebang-regex: 3.0.0\n\n  /shebang-regex@3.0.0:\n    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}\n    engines: {node: '>=8'}\n\n  /shell-quote@1.8.3:\n    resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}\n    engines: {node: '>= 0.4'}\n    dev: false\n\n  /signal-exit@3.0.7:\n    resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}\n    dev: false\n\n  /signal-exit@4.1.0:\n    resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}\n    engines: {node: '>=14'}\n    dev: false\n\n  /simple-plist@1.3.1:\n    resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==}\n    dependencies:\n      bplist-creator: 0.1.0\n      bplist-parser: 0.3.1\n      plist: 3.1.0\n    dev: false\n\n  /simple-swizzle@0.2.4:\n    resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}\n    dependencies:\n      is-arrayish: 0.3.4\n    dev: false\n\n  /sisteransi@1.0.5:\n    resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}\n    dev: false\n\n  /slash@3.0.0:\n    resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /slugify@1.6.6:\n    resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}\n    engines: {node: '>=8.0.0'}\n    dev: false\n\n  /snake-case@3.0.4:\n    resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}\n    dependencies:\n      dot-case: 3.0.4\n      tslib: 2.8.1\n    dev: false\n\n  /source-map-js@1.2.1:\n    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /source-map-support@0.5.21:\n    resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}\n    dependencies:\n      buffer-from: 1.1.2\n      source-map: 0.6.1\n    dev: false\n\n  /source-map@0.5.7:\n    resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /source-map@0.6.1:\n    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /split-on-first@1.1.0:\n    resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}\n    engines: {node: '>=6'}\n    dev: false\n\n  /sprintf-js@1.0.3:\n    resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}\n    dev: false\n\n  /stack-utils@2.0.6:\n    resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}\n    engines: {node: '>=10'}\n    dependencies:\n      escape-string-regexp: 2.0.0\n    dev: false\n\n  /stackframe@1.3.4:\n    resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}\n    dev: false\n\n  /stacktrace-parser@0.1.11:\n    resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}\n    engines: {node: '>=6'}\n    dependencies:\n      type-fest: 0.7.1\n    dev: false\n\n  /statuses@1.5.0:\n    resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}\n    engines: {node: '>= 0.6'}\n    dev: false\n\n  /statuses@2.0.1:\n    resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /stream-buffers@2.2.0:\n    resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}\n    engines: {node: '>= 0.10.0'}\n    dev: false\n\n  /strict-uri-encode@2.0.0:\n    resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /string-width@4.2.3:\n    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}\n    engines: {node: '>=8'}\n    dependencies:\n      emoji-regex: 8.0.0\n      is-fullwidth-code-point: 3.0.0\n      strip-ansi: 6.0.1\n    dev: false\n\n  /string-width@5.1.2:\n    resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}\n    engines: {node: '>=12'}\n    dependencies:\n      eastasianwidth: 0.2.0\n      emoji-regex: 9.2.2\n      strip-ansi: 7.1.2\n    dev: false\n\n  /strip-ansi@5.2.0:\n    resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}\n    engines: {node: '>=6'}\n    dependencies:\n      ansi-regex: 4.1.1\n    dev: false\n\n  /strip-ansi@6.0.1:\n    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}\n    engines: {node: '>=8'}\n    dependencies:\n      ansi-regex: 5.0.1\n    dev: false\n\n  /strip-ansi@7.1.2:\n    resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}\n    engines: {node: '>=12'}\n    dependencies:\n      ansi-regex: 6.2.2\n    dev: false\n\n  /strip-json-comments@2.0.1:\n    resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}\n    engines: {node: '>=0.10.0'}\n    dev: false\n\n  /structured-headers@0.4.1:\n    resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}\n    dev: false\n\n  /sucrase@3.35.0:\n    resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}\n    engines: {node: '>=16 || 14 >=14.17'}\n    hasBin: true\n    dependencies:\n      '@jridgewell/gen-mapping': 0.3.13\n      commander: 4.1.1\n      glob: 10.4.5\n      lines-and-columns: 1.2.4\n      mz: 2.7.0\n      pirates: 4.0.7\n      ts-interface-checker: 0.1.13\n    dev: false\n\n  /superjson@2.2.2:\n    resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}\n    engines: {node: '>=16'}\n    dependencies:\n      copy-anything: 3.0.5\n    dev: false\n\n  /supports-color@5.5.0:\n    resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}\n    engines: {node: '>=4'}\n    dependencies:\n      has-flag: 3.0.0\n    dev: false\n\n  /supports-color@7.2.0:\n    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}\n    engines: {node: '>=8'}\n    dependencies:\n      has-flag: 4.0.0\n    dev: false\n\n  /supports-color@8.1.1:\n    resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}\n    engines: {node: '>=10'}\n    dependencies:\n      has-flag: 4.0.0\n    dev: false\n\n  /supports-hyperlinks@2.3.0:\n    resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}\n    engines: {node: '>=8'}\n    dependencies:\n      has-flag: 4.0.0\n      supports-color: 7.2.0\n    dev: false\n\n  /supports-preserve-symlinks-flag@1.0.0:\n    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}\n    engines: {node: '>= 0.4'}\n    dev: false\n\n  /svg-parser@2.0.4:\n    resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}\n    dev: false\n\n  /svgo@3.3.2:\n    resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}\n    engines: {node: '>=14.0.0'}\n    hasBin: true\n    dependencies:\n      '@trysound/sax': 0.2.0\n      commander: 7.2.0\n      css-select: 5.2.2\n      css-tree: 2.3.1\n      css-what: 6.2.2\n      csso: 5.0.5\n      picocolors: 1.1.1\n    dev: false\n\n  /tar@7.4.3:\n    resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}\n    engines: {node: '>=18'}\n    dependencies:\n      '@isaacs/fs-minipass': 4.0.1\n      chownr: 3.0.0\n      minipass: 7.1.2\n      minizlib: 3.0.2\n      mkdirp: 3.0.1\n      yallist: 5.0.0\n    dev: false\n\n  /temp-dir@2.0.0:\n    resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /terminal-link@2.1.1:\n    resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==}\n    engines: {node: '>=8'}\n    dependencies:\n      ansi-escapes: 4.3.2\n      supports-hyperlinks: 2.3.0\n    dev: false\n\n  /terser@5.44.0:\n    resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==}\n    engines: {node: '>=10'}\n    hasBin: true\n    dependencies:\n      '@jridgewell/source-map': 0.3.11\n      acorn: 8.15.0\n      commander: 2.20.3\n      source-map-support: 0.5.21\n    dev: false\n\n  /test-exclude@6.0.0:\n    resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}\n    engines: {node: '>=8'}\n    dependencies:\n      '@istanbuljs/schema': 0.1.3\n      glob: 7.2.3\n      minimatch: 3.1.2\n    dev: false\n\n  /thenify-all@1.6.0:\n    resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}\n    engines: {node: '>=0.8'}\n    dependencies:\n      thenify: 3.3.1\n    dev: false\n\n  /thenify@3.3.1:\n    resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}\n    dependencies:\n      any-promise: 1.3.0\n    dev: false\n\n  /throat@5.0.0:\n    resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==}\n    dev: false\n\n  /tmpl@1.0.5:\n    resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}\n    dev: false\n\n  /to-regex-range@5.0.1:\n    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}\n    engines: {node: '>=8.0'}\n    dependencies:\n      is-number: 7.0.0\n    dev: false\n\n  /toidentifier@1.0.1:\n    resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}\n    engines: {node: '>=0.6'}\n    dev: false\n\n  /ts-interface-checker@0.1.13:\n    resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}\n    dev: false\n\n  /tslib@2.8.1:\n    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}\n    dev: false\n\n  /type-detect@4.0.8:\n    resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /type-fest@0.21.3:\n    resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /type-fest@0.7.1:\n    resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /typescript@5.9.2:\n    resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}\n    engines: {node: '>=14.17'}\n    hasBin: true\n\n  /undici-types@7.12.0:\n    resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==}\n    dev: false\n\n  /undici@6.21.3:\n    resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}\n    engines: {node: '>=18.17'}\n    dev: false\n\n  /unicode-canonical-property-names-ecmascript@2.0.1:\n    resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /unicode-match-property-ecmascript@2.0.0:\n    resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}\n    engines: {node: '>=4'}\n    dependencies:\n      unicode-canonical-property-names-ecmascript: 2.0.1\n      unicode-property-aliases-ecmascript: 2.2.0\n    dev: false\n\n  /unicode-match-property-value-ecmascript@2.2.1:\n    resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /unicode-property-aliases-ecmascript@2.2.0:\n    resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}\n    engines: {node: '>=4'}\n    dev: false\n\n  /unique-string@2.0.0:\n    resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}\n    engines: {node: '>=8'}\n    dependencies:\n      crypto-random-string: 2.0.0\n    dev: false\n\n  /unpipe@1.0.0:\n    resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /update-browserslist-db@1.1.3(browserslist@4.26.2):\n    resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}\n    hasBin: true\n    peerDependencies:\n      browserslist: '>= 4.21.0'\n    dependencies:\n      browserslist: 4.26.2\n      escalade: 3.2.0\n      picocolors: 1.1.1\n    dev: false\n\n  /use-callback-ref@1.3.3(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}\n    engines: {node: '>=10'}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      react: 19.1.0\n      tslib: 2.8.1\n    dev: false\n\n  /use-latest-callback@0.2.4(react@19.1.0):\n    resolution: {integrity: sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg==}\n    peerDependencies:\n      react: '>=16.8'\n    dependencies:\n      react: 19.1.0\n    dev: false\n\n  /use-sidecar@1.1.3(@types/react@19.1.13)(react@19.1.0):\n    resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}\n    engines: {node: '>=10'}\n    peerDependencies:\n      '@types/react': '*'\n      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc\n    peerDependenciesMeta:\n      '@types/react':\n        optional: true\n    dependencies:\n      '@types/react': 19.1.13\n      detect-node-es: 1.1.0\n      react: 19.1.0\n      tslib: 2.8.1\n    dev: false\n\n  /use-sync-external-store@1.5.0(react@19.1.0):\n    resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}\n    peerDependencies:\n      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\n    dependencies:\n      react: 19.1.0\n    dev: false\n\n  /utils-merge@1.0.1:\n    resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}\n    engines: {node: '>= 0.4.0'}\n    dev: false\n\n  /uuid@7.0.3:\n    resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==}\n    hasBin: true\n    dev: false\n\n  /validate-npm-package-name@5.0.1:\n    resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}\n    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}\n    dev: false\n\n  /vary@1.1.2:\n    resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}\n    engines: {node: '>= 0.8'}\n    dev: false\n\n  /vaul@1.1.2(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0):\n    resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}\n    peerDependencies:\n      react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc\n      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc\n    dependencies:\n      '@radix-ui/react-dialog': 1.1.15(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.0)\n      react: 19.1.0\n      react-dom: 19.1.1(react@19.1.0)\n    transitivePeerDependencies:\n      - '@types/react'\n      - '@types/react-dom'\n    dev: false\n\n  /vlq@1.0.1:\n    resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}\n    dev: false\n\n  /walker@1.0.8:\n    resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}\n    dependencies:\n      makeerror: 1.0.12\n    dev: false\n\n  /warn-once@0.1.1:\n    resolution: {integrity: sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==}\n    dev: false\n\n  /wcwidth@1.0.1:\n    resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}\n    dependencies:\n      defaults: 1.0.4\n    dev: false\n\n  /webidl-conversions@5.0.0:\n    resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==}\n    engines: {node: '>=8'}\n    dev: false\n\n  /whatwg-fetch@3.6.20:\n    resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}\n    dev: false\n\n  /whatwg-url-without-unicode@8.0.0-3:\n    resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==}\n    engines: {node: '>=10'}\n    dependencies:\n      buffer: 5.7.1\n      punycode: 2.3.1\n      webidl-conversions: 5.0.0\n    dev: false\n\n  /which@2.0.2:\n    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}\n    engines: {node: '>= 8'}\n    hasBin: true\n    dependencies:\n      isexe: 2.0.0\n\n  /wonka@6.3.5:\n    resolution: {integrity: sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==}\n    dev: false\n\n  /wrap-ansi@7.0.0:\n    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}\n    engines: {node: '>=10'}\n    dependencies:\n      ansi-styles: 4.3.0\n      string-width: 4.2.3\n      strip-ansi: 6.0.1\n    dev: false\n\n  /wrap-ansi@8.1.0:\n    resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}\n    engines: {node: '>=12'}\n    dependencies:\n      ansi-styles: 6.2.3\n      string-width: 5.1.2\n      strip-ansi: 7.1.2\n    dev: false\n\n  /wrappy@1.0.2:\n    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}\n    dev: false\n\n  /write-file-atomic@4.0.2:\n    resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}\n    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}\n    dependencies:\n      imurmurhash: 0.1.4\n      signal-exit: 3.0.7\n    dev: false\n\n  /ws@6.2.3:\n    resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==}\n    peerDependencies:\n      bufferutil: ^4.0.1\n      utf-8-validate: ^5.0.2\n    peerDependenciesMeta:\n      bufferutil:\n        optional: true\n      utf-8-validate:\n        optional: true\n    dependencies:\n      async-limiter: 1.0.1\n    dev: false\n\n  /ws@7.5.10:\n    resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}\n    engines: {node: '>=8.3.0'}\n    peerDependencies:\n      bufferutil: ^4.0.1\n      utf-8-validate: ^5.0.2\n    peerDependenciesMeta:\n      bufferutil:\n        optional: true\n      utf-8-validate:\n        optional: true\n    dev: false\n\n  /ws@8.18.3:\n    resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}\n    engines: {node: '>=10.0.0'}\n    peerDependencies:\n      bufferutil: ^4.0.1\n      utf-8-validate: '>=5.0.2'\n    peerDependenciesMeta:\n      bufferutil:\n        optional: true\n      utf-8-validate:\n        optional: true\n    dev: false\n\n  /xcode@3.0.1:\n    resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==}\n    engines: {node: '>=10.0.0'}\n    dependencies:\n      simple-plist: 1.3.1\n      uuid: 7.0.3\n    dev: false\n\n  /xml2js@0.6.0:\n    resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}\n    engines: {node: '>=4.0.0'}\n    dependencies:\n      sax: 1.4.1\n      xmlbuilder: 11.0.1\n    dev: false\n\n  /xmlbuilder@11.0.1:\n    resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}\n    engines: {node: '>=4.0'}\n    dev: false\n\n  /xmlbuilder@15.1.1:\n    resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}\n    engines: {node: '>=8.0'}\n    dev: false\n\n  /y18n@5.0.8:\n    resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /yallist@3.1.1:\n    resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}\n    dev: false\n\n  /yallist@5.0.0:\n    resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}\n    engines: {node: '>=18'}\n    dev: false\n\n  /yaml@2.8.1:\n    resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}\n    engines: {node: '>= 14.6'}\n    hasBin: true\n    dev: false\n\n  /yargs-parser@21.1.1:\n    resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}\n    engines: {node: '>=12'}\n    dev: false\n\n  /yargs@17.7.2:\n    resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}\n    engines: {node: '>=12'}\n    dependencies:\n      cliui: 8.0.1\n      escalade: 3.2.0\n      get-caller-file: 2.0.5\n      require-directory: 2.1.1\n      string-width: 4.2.3\n      y18n: 5.0.8\n      yargs-parser: 21.1.1\n    dev: false\n\n  /yocto-queue@0.1.0:\n    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}\n    engines: {node: '>=10'}\n    dev: false\n\n  /zod-to-json-schema@3.24.6(zod@3.25.76):\n    resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==}\n    peerDependencies:\n      zod: ^3.24.1\n    dependencies:\n      zod: 3.25.76\n    dev: false\n\n  /zod@3.25.76:\n    resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}\n    dev: false\n"
  },
  {
    "path": "apps/react-native-expo/shared/api/api-root/api-root-instanse.ts",
    "content": "import axios, {\n  AxiosError, // CHANGED: добавил для нормализации ошибок\n  AxiosHeaders,\n  AxiosInstance,\n  AxiosRequestConfig,\n  AxiosResponse,\n  InternalAxiosRequestConfig,\n} from 'axios'\n\nimport { API_KEY, API_PREFIX_ROOT, API_ROOT, VERSION_ROOT } from '@/shared/api/api-root/api-root'\nimport { RETRY_HEADER_CONST } from '@/shared/consts/consts'\nimport { tokenStorage } from '@/shared/storage/tokenStorage'\nimport { makeFullUrl } from '@/shared/utils/makeFullUrl'\n\ntype ApiPrefixT = (typeof API_PREFIX_ROOT)[keyof typeof API_PREFIX_ROOT]\n\nlet refreshTokenPromise: Promise<void> | null = null\nconst RETRY_HEADER = RETRY_HEADER_CONST\n\nconst normalizeAxiosError = (e: unknown): AxiosError => {\n  if (axios.isAxiosError(e)) return e\n  const err = new AxiosError(typeof e === 'string' ? e : 'Unknown error')\n  ;(err as any).cause = e\n  return err\n}\n\nconst makeRefreshUrl = () => {\n  const base = (API_ROOT || '').replace(/\\/+$/, '')\n  const ver = String(VERSION_ROOT).replace(/^\\/+|\\/+$/g, '') // CHANGED: режем слэши по краям\n  const pfxAuth = String(API_PREFIX_ROOT.AUTH).replace(/^\\/+|\\/+$/g, '')\n  return `${base}/${ver}/${pfxAuth}/refresh`\n}\n\nconst ensureRefresh = (): Promise<void> => {\n  if (!refreshTokenPromise) {\n    refreshTokenPromise = (async () => {\n      const refreshToken = await tokenStorage.getRefresh()\n      if (!refreshToken) {\n        console.warn('Refresh токен не найден')\n      }\n\n      const url = makeRefreshUrl()\n      const res = await axios.post(\n        url,\n        { refreshToken },\n        {\n          headers: {\n            'Content-Type': 'application/json',\n            ...(API_KEY ? { 'api-key': API_KEY } : {}),\n            Origin: 'http://localhost:3000',\n          },\n        }\n      )\n\n      const data = res.data as { accessToken?: string; refreshToken?: string }\n      await tokenStorage.set({\n        accessToken: data?.accessToken ?? '',\n        refreshToken: data?.refreshToken ?? '',\n      })\n    })()\n      .catch(async (e) => {\n        const err = normalizeAxiosError(e)\n        console.warn('REFRESH ERROR:', err.message)\n        await tokenStorage.clear()\n        console.warn(err)\n      })\n      .finally(() => {\n        refreshTokenPromise = null\n      })\n  }\n  return refreshTokenPromise\n}\n\nexport const httpApiInterceptor = (prefix: ApiPrefixT, version = VERSION_ROOT): AxiosInstance => {\n  const base = (API_ROOT || '').replace(/\\/+$/, '')\n  const pfx = String(prefix).replace(/^\\/+|\\/+$/g, '')\n  const ver = String(version).replace(/^\\/+|\\/+$/g, '')\n\n  const instance = axios.create({\n    baseURL: `${base}/${ver}/${pfx}`,\n    headers: {\n      'Content-Type': 'application/json',\n      Origin: 'http://localhost:3000',\n      ...(API_KEY ? { 'api-key': API_KEY } : {}),\n    },\n  })\n\n  instance.interceptors.request.use(\n    async (config: InternalAxiosRequestConfig) => {\n      const method = (config.method || 'get').toUpperCase()\n      // CHANGED: для логов используем helper, а не конкатенацию baseURL+url\n      const rUrl = makeFullUrl(config.baseURL, config.url)\n      console.log('REQUEST:', method, rUrl)\n      if (config.params) console.log('params:', config.params)\n      if (config.data) console.log('body  :', config.data)\n\n      const headers = AxiosHeaders.from(config.headers || {})\n      headers.set('Origin', 'http://localhost:3000')\n      if (API_KEY) headers.set('api-key', API_KEY)\n\n      const accessToken = await tokenStorage.getAccess()\n      if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`)\n\n      if (typeof config.url === 'string') {\n        config.url = '/' + config.url.replace(/^\\/+/, '')\n      }\n\n      config.headers = headers\n      return config\n    },\n    (error) => {\n      console.log('REQUEST ERROR:', error?.message)\n      return Promise.reject(normalizeAxiosError(error)) // CHANGED\n    }\n  )\n\n  instance.interceptors.response.use(\n    (res: AxiosResponse) => {\n      const method = (res.config.method || 'get').toUpperCase()\n      const full = makeFullUrl(res.config.baseURL, res.config.url)\n      console.log('RESPONSE:', res.status, method, full)\n      return res\n    },\n    async (error) => {\n      const err = normalizeAxiosError(error)\n      const cfg = (err.config || {}) as InternalAxiosRequestConfig\n\n      const method = (cfg.method || 'get').toUpperCase()\n      const full = makeFullUrl(cfg.baseURL, cfg.url)\n      const status = err.response?.status ?? 'NET-ERR'\n      console.log('RESPONSE ERROR:', status, method, full)\n      console.log('msg:', err.message)\n\n      if (err.response?.status !== 401) return Promise.reject(err)\n\n      const localUrl = String(cfg.url || '')\n      if (/\\/auth\\/login/i.test(localUrl) || /\\/auth\\/refresh/i.test(localUrl)) {\n        return Promise.reject(err)\n      }\n\n      const hdrs = AxiosHeaders.from(cfg.headers || {})\n      if (hdrs.get(RETRY_HEADER) === '1') return Promise.reject(err)\n\n      try {\n        await ensureRefresh()\n\n        const newAccess = (await tokenStorage.getAccess()) || ''\n        hdrs.set('Origin', 'http://localhost:3000')\n        if (API_KEY) hdrs.set('api-key', API_KEY)\n        if (newAccess) hdrs.set('Authorization', `Bearer ${newAccess}`)\n        hdrs.set(RETRY_HEADER, '1')\n\n        const retryConfig: AxiosRequestConfig = {\n          ...cfg,\n          headers: hdrs,\n          url: '/' + String(cfg.url || '').replace(/^\\/+/, ''),\n        }\n\n        const rMethod = (retryConfig.method || 'get').toUpperCase()\n        const rFull = makeFullUrl(retryConfig.baseURL as string, retryConfig.url as string)\n        console.log('RETRY REQUEST:', rMethod, rFull)\n        if (retryConfig.params) console.log('params:', retryConfig.params)\n        if (retryConfig.data) console.log('body  :', retryConfig.data)\n\n        return instance.request(retryConfig)\n      } catch (e) {\n        const rfErr = normalizeAxiosError(e)\n        console.error('refresh failed:', rfErr.message)\n        return Promise.reject(err)\n      }\n    }\n  )\n\n  return instance\n}\n"
  },
  {
    "path": "apps/react-native-expo/shared/api/api-root/api-root.ts",
    "content": "import Constants from 'expo-constants'\n\ntype Extra = {\n  API_BASE_URL?: string\n  API_API_KEY?: string\n  env?: 'development' | 'production'\n}\n\nconst extra = (Constants.expoConfig?.extra ?? {}) as Extra\n\nif (!extra.API_BASE_URL) {\n  console.warn(`Отсутствует API_BASE_URL , запросы будут падать `)\n} else {\n  console.log('API_BASE_URL обнаружен:', extra.API_BASE_URL)\n}\n\nif (!extra.API_API_KEY) {\n  console.warn(\n    `Отсутствует API_API_KEY, получите его на ресурсе https://apihub.it-incubator.io/en/2`\n  )\n} else {\n  console.log('API_API_KEY обнаружен: ********************' /** extra.API_API_KEY */)\n}\n\nconsole.log('Режим работы .env через:', `.env.${extra.env}`)\n\nexport const API_ROOT = (extra.API_BASE_URL ?? '').trim()\nexport const API_KEY = (extra.API_API_KEY ?? '').trim()\nexport const VERSION_ROOT = '1.0'\n\nexport const API_PREFIX_ROOT = {\n  TEST: 'test',\n  AUTH: 'auth',\n  PLAYLISTS: 'playlists',\n} as const\n"
  },
  {
    "path": "apps/react-native-expo/shared/api/query-client/queryClient.ts",
    "content": "import { QueryClient } from '@tanstack/react-query'\n\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 10_000,\n      refetchOnWindowFocus: false, // в RN окна нет\n      refetchOnMount: false,\n      refetchOnReconnect: false,\n      // gcTime: 5_000,\n      retry: 1,\n    },\n  },\n})\n"
  },
  {
    "path": "apps/react-native-expo/shared/api/query-persist/query-presist.ts",
    "content": "import AsyncStorage from '@react-native-async-storage/async-storage'\nimport { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'\nimport { persistQueryClient } from '@tanstack/react-query-persist-client'\n\nimport { queryClient } from '@/shared/api/query-client/queryClient'\nimport { KEY_STORAGE } from '@/shared/consts/key-storage/key-storage'\n\n//позволяет сохранять кэш запросов не только в памяти приложения, но и во внешнем хранилище\n\n//Зачем?\n// Чтобы при перезапуске приложения или даже при уходе приложения в фон данные запросов не исчезали.\n// Пользователь получает более быстрый старт, меньше лишних сетевых запросов, и оффлайн-режим работает лучше.\nconst persister = createAsyncStoragePersister({\n  storage: AsyncStorage,\n  key: KEY_STORAGE.RQ_CACHE,\n  throttleTime: 1000, //интервал между сохранениями в мс\n})\n\nexport function setupQueryPersist() {\n  persistQueryClient({\n    queryClient,\n    persister,\n    maxAge: 24 * 60 * 60 * 1000, //24 часа\n  })\n}\n"
  },
  {
    "path": "apps/react-native-expo/shared/consts/consts.ts",
    "content": "export const CALLBACK_URL = 'http://localhost:8081/callback'\nexport const RETRY_HEADER_CONST = 'x-musicfun-retried'\n"
  },
  {
    "path": "apps/react-native-expo/shared/consts/key-storage/key-storage.ts",
    "content": "export const KEY_STORAGE = {\n  RQ_CACHE: 'musicfun-rq-cache',\n  AUTH_TOKEN: 'musicfun-auth-token',\n  ACCESS_TOKEN: 'musicfun-access-token',\n  REFRESH_TOKEN: 'musicfun-refresh-token',\n}\n"
  },
  {
    "path": "apps/react-native-expo/shared/providers/reactQueryProviders/ReactQueryProviders.tsx",
    "content": "import NetInfo from '@react-native-community/netinfo'\nimport { focusManager, onlineManager, QueryClientProvider } from '@tanstack/react-query'\nimport { PropsWithChildren, useEffect } from 'react'\nimport { AppState } from 'react-native'\n\nimport { queryClient } from '@/shared/api/query-client/queryClient'\nimport { setupQueryPersist } from '@/shared/api/query-persist/query-presist'\n\nsetupQueryPersist()\n\nexport function ReactQueryProvider({ children }: PropsWithChildren) {\n  //имитирует «фокус окна» браузера для мобильного приложения.\n\n  //focusManager.setFocused(true) — для React Query это означает\n  // «приложение на переднем плане»: можно возобновлять фоновые процессы, разрешать рефетчи «на фокусе» и т.п.\n  //focusManager.setFocused(false) — «в фоне»: пауза некоторых операций (например, повторов), уважение опций наподобие refetchOnWindowFocus.\n  useEffect(() => {\n    const sub = AppState.addEventListener('change', (state) => {\n      console.log('[AppState]', state)\n      focusManager.setFocused(state === 'active')\n    })\n    return () => sub.remove()\n  }, [])\n\n  //Сообщает React Query текущий статус «онлайн/офлайн».\n  //Когда offline: остановятся ретраи, не будут стартовать новые фетчи.\n  // Когда online: запросы с refetchOnReconnect: true автоматически обновятся.\n  useEffect(() => {\n    return onlineManager.setEventListener((setOnline) =>\n      NetInfo.addEventListener((s) => {\n        const online = !!(s.isConnected && s.isInternetReachable)\n        console.log('[NetInfo]', {\n          online,\n          type: s.type,\n          isConnected: s.isConnected,\n          isInternetReachable: s.isInternetReachable,\n        })\n        setOnline(online)\n      })\n    )\n  }, [])\n\n  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n}\n"
  },
  {
    "path": "apps/react-native-expo/shared/storage/tokenStorage.ts",
    "content": "import * as SecureStore from 'expo-secure-store'\n\nimport { KEY_STORAGE } from '@/shared/consts/key-storage/key-storage'\n\nexport type Tokens = { accessToken: string; refreshToken: string }\n\nexport const tokenStorage = {\n  set: async ({ accessToken, refreshToken }: Tokens) => {\n    await Promise.all([\n      SecureStore.setItemAsync(KEY_STORAGE.ACCESS_TOKEN, accessToken, {\n        keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK,\n      }),\n      SecureStore.setItemAsync(KEY_STORAGE.REFRESH_TOKEN, refreshToken, {\n        keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK,\n      }),\n    ])\n  },\n  getAccess: () => SecureStore.getItemAsync(KEY_STORAGE.ACCESS_TOKEN),\n  getRefresh: () => SecureStore.getItemAsync(KEY_STORAGE.REFRESH_TOKEN),\n  clear: async () => {\n    await Promise.all([\n      SecureStore.deleteItemAsync(KEY_STORAGE.ACCESS_TOKEN),\n      SecureStore.deleteItemAsync(KEY_STORAGE.REFRESH_TOKEN),\n    ])\n  },\n}\n"
  },
  {
    "path": "apps/react-native-expo/shared/styles/tokens.ts",
    "content": "export const COLORS = {\n  DARK: {\n    BACKGROUND_MAIN: '#121212',\n    BACKGROUND_ELEMENTS_1: '#07070780',\n    BACKGROUND_ELEMENTS_2: '#141414',\n    BACKGROUND_ELEMENTS_3: '#51173C',\n    DEEP_DARK: '#000000',\n\n    BUTTON_MAIN_GRAY: '#555555',\n    BUTTON_MAIN_GRAY_HOVER: '#444444',\n    BUTTON_MAIN_PINK: '#FF38B6',\n    BUTTON_MAIN_PINK_HOVER: '#ff02a0',\n\n    TEXT_MAIN_WHITE: '#FFFFFF',\n    TEXT_MAIN_GRAY: '#B3B3B3',\n  },\n  LIGHT: {},\n}\n\nexport const GAPS = {\n  G8: 8,\n  G27: 27,\n}\n\nexport const RADIUS = {\n  R45: 45,\n}\n\nexport const FONTS_SIZES = {\n  f12: 12,\n  f13: 13,\n  f14: 14,\n  f16: 16,\n  f18: 18,\n}\n"
  },
  {
    "path": "apps/react-native-expo/shared/styles/tokens.type.ts",
    "content": "import { COLORS, FONTS_SIZES, GAPS, RADIUS } from '@/shared/styles/tokens'\n\nexport type ColorTheme = keyof typeof COLORS\nexport type GapName = keyof typeof GAPS\nexport type RadiusName = keyof typeof RADIUS\nexport type FontName = keyof typeof FONTS_SIZES\n\nexport type DarkColorKey = keyof (typeof COLORS)['DARK']\nexport type LightColorKey = keyof (typeof COLORS)['LIGHT']\n"
  },
  {
    "path": "apps/react-native-expo/shared/ui/Button/Button.tsx",
    "content": "import React from 'react'\nimport {\n  ActivityIndicator,\n  Animated,\n  GestureResponderEvent,\n  Pressable,\n  StyleSheet,\n  Text,\n} from 'react-native'\n\nimport { COLORS, FONTS_SIZES, RADIUS } from '@/shared/styles/tokens'\nimport { ButtonProps } from '@/shared/ui/Button/Button.type'\n\nexport const Button = ({\n  title,\n  buttonStyle,\n  textStyle,\n  onPressIn,\n  onPressOut,\n  isLoading,\n  isFull,\n  variant = 'primary',\n  disabled,\n  ...props\n}: ButtonProps) => {\n  const bg = React.useRef(new Animated.Value(0)).current\n  const scale = React.useRef(new Animated.Value(1)).current\n\n  const [fromColor, toColor] =\n    variant === 'gray'\n      ? [\n          COLORS.DARK.BUTTON_MAIN_GRAY,\n          COLORS.DARK.BUTTON_MAIN_GRAY_HOVER ?? COLORS.DARK.BUTTON_MAIN_GRAY,\n        ]\n      : [\n          COLORS.DARK.BUTTON_MAIN_PINK,\n          COLORS.DARK.BUTTON_MAIN_PINK_HOVER ?? COLORS.DARK.BUTTON_MAIN_PINK,\n        ]\n\n  const bgColor = bg.interpolate({ inputRange: [0, 1], outputRange: [fromColor, toColor] })\n\n  const handlePressIn = (e: GestureResponderEvent) => {\n    if (disabled) return\n    Animated.timing(bg, { toValue: 1, duration: 100, useNativeDriver: false }).start()\n    Animated.spring(scale, { toValue: 0.95, useNativeDriver: true }).start()\n    onPressIn?.(e)\n  }\n\n  const handlePressOut = (e: GestureResponderEvent) => {\n    if (disabled) return\n    Animated.timing(bg, { toValue: 0, duration: 100, useNativeDriver: false }).start()\n    Animated.spring(scale, { toValue: 1, friction: 3, tension: 40, useNativeDriver: true }).start()\n    onPressOut?.(e)\n  }\n\n  const width = isFull ? '100%' : 328\n  const height = 51\n\n  return (\n    <Pressable\n      {...props}\n      disabled={disabled}\n      style={[{ width }, buttonStyle]}\n      onPressIn={handlePressIn}\n      onPressOut={handlePressOut}>\n      <Animated.View style={[styles.shell, { width, height, transform: [{ scale }] }]}>\n        <Animated.View\n          style={[styles.fill, { backgroundColor: bgColor, opacity: disabled ? 0.6 : 1 }]}\n        />\n        {!isLoading && <Text style={[styles.text, textStyle]}>{title}</Text>}\n        {isLoading && <ActivityIndicator size=\"small\" color={COLORS.DARK.TEXT_MAIN_WHITE} />}\n      </Animated.View>\n    </Pressable>\n  )\n}\n\nconst styles = StyleSheet.create({\n  shell: {\n    justifyContent: 'center',\n    alignItems: 'center',\n    borderRadius: RADIUS.R45,\n    overflow: 'hidden',\n  },\n  fill: {\n    ...StyleSheet.absoluteFillObject,\n    borderRadius: RADIUS.R45,\n  },\n  text: {\n    fontSize: FONTS_SIZES.f16,\n    color: COLORS.DARK.TEXT_MAIN_WHITE,\n    fontFamily: 'Lato-Bold',\n  },\n})\n"
  },
  {
    "path": "apps/react-native-expo/shared/ui/Button/Button.type.tsx",
    "content": "import { PressableProps, StyleProp, TextStyle, ViewStyle } from 'react-native'\n\nexport type ButtonProps = PressableProps & {\n  title: string\n  buttonStyle?: StyleProp<ViewStyle>\n  textStyle?: StyleProp<TextStyle>\n  isLoading?: boolean\n  isFull?: boolean\n  variant?: 'primary' | 'gray'\n  disabled?: boolean\n}\n"
  },
  {
    "path": "apps/react-native-expo/shared/ui/Icons/navigation/IcAllPlaylist.tsx",
    "content": "import * as React from 'react'\nimport Svg, { Path, SvgProps } from 'react-native-svg'\n\nexport const IcAllPlaylist = (props: SvgProps) => (\n  <Svg fill=\"none\" {...props}>\n    <Path\n      fill=\"#fff\"\n      d=\"M27 0H3A2.675 2.675 0 0 0 .333 2.667v18.666C.333 22.8 1.533 24 3 24h24c1.467 0 2.667-1.2 2.667-2.667V2.667C29.667 1.2 28.467 0 27 0Zm0 21.333H3V2.667h24v18.666ZM9.667 16c0-2.213 1.786-4 4-4 .466 0 .92.093 1.333.24V4h6.667v2.667h-4v9.373a4.003 4.003 0 0 1-4 3.96c-2.214 0-4-1.787-4-4Z\"\n    />\n  </Svg>\n)\n"
  },
  {
    "path": "apps/react-native-expo/shared/ui/Icons/navigation/IcAllTracks.tsx",
    "content": "import * as React from 'react'\nimport Svg, { Path, SvgProps } from 'react-native-svg'\n\nexport const IcAllTracks = (props: SvgProps) => (\n  <Svg fill=\"none\" {...props}>\n    <Path\n      fill=\"#fff\"\n      d=\"m8.5 0 .013 14.067a5.329 5.329 0 0 0-2.666-.734A5.335 5.335 0 0 0 .5 18.667 5.335 5.335 0 0 0 5.847 24c2.96 0 5.32-2.387 5.32-5.333V5.333H16.5V0h-8ZM5.847 21.333a2.674 2.674 0 0 1-2.667-2.666C3.18 17.2 4.38 16 5.847 16c1.466 0 2.666 1.2 2.666 2.667 0 1.466-1.2 2.666-2.666 2.666Z\"\n    />\n  </Svg>\n)\n"
  },
  {
    "path": "apps/react-native-expo/shared/ui/Icons/navigation/IcHome.tsx",
    "content": "import * as React from 'react'\nimport Svg, { Path, SvgProps } from 'react-native-svg'\n\nexport const IcHome = (props: SvgProps) => (\n  <Svg fill=\"none\" {...props}>\n    <Path\n      fill=\"#fff\"\n      d=\"m13.5 3.587 6.667 6V20H17.5v-8h-8v8H6.833V9.587l6.667-6ZM13.5 0 .167 12h4v10.667h8v-8h2.666v8h8V12h4L13.5 0Z\"\n    />\n  </Svg>\n)\n"
  },
  {
    "path": "apps/react-native-expo/shared/ui/Icons/navigation/IcYourLibrary.tsx",
    "content": "import * as React from 'react'\nimport Svg, { Path, SvgProps } from 'react-native-svg'\n\nexport const IcYourLibrary = (props: SvgProps) => (\n  <Svg fill=\"none\" {...props}>\n    <Path\n      fill=\"#fff\"\n      d=\"M24.167.667h-16A2.675 2.675 0 0 0 5.5 3.333v16C5.5 20.8 6.7 22 8.167 22h16c1.466 0 2.666-1.2 2.666-2.667v-16c0-1.466-1.2-2.666-2.666-2.666Zm0 16.666a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12Zm-10 .667a3.335 3.335 0 0 0 3.333-3.333V9.333a2 2 0 0 1 2-2h.667a1.333 1.333 0 1 0 0-2.666h-2a2 2 0 0 0-2 2v3.195c0 .882-1.119 1.471-2 1.471a3.334 3.334 0 0 0 0 6.667ZM2.833 7.333a1.333 1.333 0 1 0-2.666 0v17.334c0 1.466 1.2 2.666 2.666 2.666h17.334a1.333 1.333 0 1 0 0-2.666H4.833a2 2 0 0 1-2-2V7.333Z\"\n    />\n  </Svg>\n)\n"
  },
  {
    "path": "apps/react-native-expo/shared/ui/Icons/screens/login/IcSmile.tsx",
    "content": "import * as React from 'react'\nimport Svg, { Circle, Ellipse, Path, SvgProps } from 'react-native-svg'\n\nexport const IcSmile = ({ width = 89, height = 89, ...props }: SvgProps) => (\n  <Svg width={width} height={height} viewBox=\"0 0 89 89\" fill=\"none\" {...props}>\n    <Circle cx={44.5} cy={44.5} r={44.5} fill=\"#FF38B6\" />\n    <Ellipse cx={58.865} cy={26.477} rx={8.431} ry={10.902} fill=\"#fff\" />\n    <Ellipse cx={57.992} cy={28.949} rx={3.198} ry={4.07} fill=\"#000\" />\n    <Ellipse cx={29.791} cy={26.477} rx={8.431} ry={10.902} fill=\"#fff\" />\n    <Ellipse cx={31.826} cy={28.949} rx={3.198} ry={4.07} fill=\"#000\" />\n    <Path\n      d=\"M64.915 69.503C54.958 80.1 35.6 80.1 24.03 68.816\"\n      stroke=\"#fff\"\n      strokeWidth={6.154}\n      strokeLinecap=\"round\"\n    />\n    <Path\n      d=\"M71.31 49.806C58.385 63.56 33.261 63.56 18.244 48.914\"\n      stroke=\"#fff\"\n      strokeWidth={7.987}\n      strokeLinecap=\"round\"\n    />\n  </Svg>\n)\n"
  },
  {
    "path": "apps/react-native-expo/shared/utils/makeFullUrl.ts",
    "content": "export const makeFullUrl = (base?: string, path?: string): string => {\n  const b = (base || '').replace(/\\/+$/, '')\n  const p = (path || '').replace(/^\\/+/, '')\n  if (!b && !p) return ''\n  if (!b) return '/' + p\n  if (!p) return b\n  return `${b}/${p}`\n}\n"
  },
  {
    "path": "apps/react-native-expo/tsconfig.json",
    "content": "{\n  \"extends\": \"expo/tsconfig.base\",\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"*\"]\n    },\n    \"moduleResolution\": \"Bundler\",\n    \"jsx\": \"react-jsx\",\n    \"types\": [\"react\", \"react-native\"],\n    \"skipLibCheck\": true,\n    \"moduleSuffixes\": [\".ios\", \".android\", \"\"]\n  },\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"declarations.d.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/reatom/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.cursor\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n*storybook.log\nstorybook-static\n"
  },
  {
    "path": "apps/reatom/.storybook/main.ts",
    "content": "import type { StorybookConfig } from '@storybook/react-vite'\n\nconst config: StorybookConfig = {\n  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],\n  addons: [],\n  framework: {\n    name: '@storybook/react-vite',\n    options: {},\n  },\n}\nexport default config\n"
  },
  {
    "path": "apps/reatom/.storybook/preview.tsx",
    "content": "import '../src/app/styles/fonts.css'\nimport '../src/app/styles/variables.css'\nimport '../src/app/styles/reset.css'\nimport '../src/app/styles/global.css'\n\nimport type { Preview } from '@storybook/react-vite'\nimport React from 'react'\nimport { BrowserRouter } from 'react-router'\n\nconst preview: Preview = {\n  parameters: {\n    controls: {\n      matchers: {\n        color: /(background|color)$/i,\n        date: /Date$/i,\n      },\n    },\n  },\n  decorators: [\n    (Story) => (\n      <BrowserRouter>\n        <Story />\n      </BrowserRouter>\n    ),\n  ],\n}\n\nexport default preview\n"
  },
  {
    "path": "apps/reatom/README.md",
    "content": "UI for Musicfun app without libs\n\nTODO:\n[] Add common components for TrackOverview and PlaylistOverview\n[] Refactor DropdownMenu\n"
  },
  {
    "path": "apps/reatom/eslint.config.js",
    "content": "// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format\nimport js from '@eslint/js'\nimport prettier from 'eslint-config-prettier'\nimport eslintPluginPrettier from 'eslint-plugin-prettier'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport simpleImportSort from 'eslint-plugin-simple-import-sort'\nimport storybook from 'eslint-plugin-storybook'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config(\n  { ignores: ['dist'] },\n  {\n    extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],\n    files: ['**/*.{ts,tsx}'],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n    plugins: {\n      'react-hooks': reactHooks,\n      'react-refresh': reactRefresh,\n      prettier: eslintPluginPrettier,\n      'simple-import-sort': simpleImportSort,\n    },\n    rules: {\n      ...reactHooks.configs.recommended.rules,\n      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],\n      'prettier/prettier': 'warn',\n      'simple-import-sort/imports': 'error',\n      'simple-import-sort/exports': 'error',\n    },\n  },\n  storybook.configs['flat/recommended']\n)\n"
  },
  {
    "path": "apps/reatom/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Musicfun Reatom</title>\n    <!-- SPA redirect handler for GitHub Pages -->\n    <script>\n      ;(function () {\n        var redirect = sessionStorage.redirect\n        delete sessionStorage.redirect\n        if (redirect && redirect !== location.href) {\n          history.replaceState(null, null, redirect)\n        }\n\n        // Check for spa_redirect query parameter\n        var searchParams = new URLSearchParams(window.location.search)\n        var spaRedirect = searchParams.get('spa_redirect')\n        if (spaRedirect) {\n          searchParams.delete('spa_redirect')\n          var newSearch = searchParams.toString()\n          var newUrl = decodeURIComponent(spaRedirect)\n          sessionStorage.redirect = newUrl\n          window.location.replace(window.location.pathname + (newSearch ? '?' + newSearch : ''))\n        }\n      })()\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/app/entrypoint/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/reatom/package.json",
    "content": "{\n  \"name\": \"musicfun-ui-vanilla\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"lint:css\": \"stylelint \\\"src/**/*.{css,scss}\\\"\",\n    \"lint:css:fix\": \"stylelint \\\"src/**/*.{css,scss}\\\" --fix\",\n    \"format\": \"prettier --write .\",\n    \"preview\": \"vite preview\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"build-storybook\": \"storybook build\",\n    \"generate:api\": \"pnpm openapi-typescript https://musicfun.it-incubator.app/api-json -o ./src/shared/api/schema.ts --root-types --enum --enum-values --dedupe-enums\",\n    \"generate:api:dimych\": \"pnpm openapi-typescript http://localhost:9001/api-json -o ./src/shared/api/schema.ts --root-types\"\n  },\n  \"dependencies\": {\n    \"@reatom/core\": \"1000.0.0-alpha.34\",\n    \"@reatom/react\": \"1000.0.0-alpha.32\",\n    \"@tanstack/react-query\": \"^5.81.5\",\n    \"@tanstack/react-query-devtools\": \"^5.81.5\",\n    \"openapi-fetch\": \"^0.14.0\",\n    \"react\": \"19.1.0\",\n    \"react-dom\": \"19.1.0\",\n    \"react-hook-form\": \"^7.60.0\",\n    \"react-router\": \"7.6.2\",\n    \"react-toastify\": \"^11.0.5\"\n  },\n  \"devDependencies\": {\n    \"@storybook/react-vite\": \"9.0.8\",\n    \"@types/node\": \"^24.0.1\",\n    \"@types/react\": \"^19.1.2\",\n    \"@types/react-dom\": \"^19.1.2\",\n    \"@vitejs/plugin-react\": \"^4.4.1\",\n    \"clsx\": \"^2.1.1\",\n    \"openapi-typescript\": \"^7.8.0\",\n    \"storybook\": \"9.0.8\",\n    \"stylelint\": \"^16.20.0\",\n    \"stylelint-config-clean-order\": \"^7.0.0\",\n    \"stylelint-config-standard\": \"^38.0.0\",\n    \"typescript\": \"~5.8.3\",\n    \"vite\": \"^6.3.5\"\n  }\n}\n"
  },
  {
    "path": "apps/reatom/src/app/App.tsx",
    "content": "import { ToastContainer } from 'react-toastify'\n\nimport { Routing } from './routing'\n\nexport const App = () => {\n  return (\n    <>\n      <Routing />\n      <ToastContainer />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/app/entrypoint/main.tsx",
    "content": "import '@/app/styles/fonts.css'\nimport '@/app/styles/variables.css'\nimport '@/app/styles/reset.css'\nimport '@/app/styles/global.css'\nimport 'react-toastify/dist/ReactToastify.css'\n\nimport { QueryClientProvider } from '@tanstack/react-query'\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { BrowserRouter } from 'react-router'\nimport { toast } from 'react-toastify'\n\nimport { queryClient } from '@/app/query-client/query-client.tsx'\nimport { localStorageKeys } from '@/features/auth/types/auth-api.types.ts'\nimport { setClientConfig } from '@/shared/api/client.ts'\nimport { API_BASE_URL, API_KEY } from '@/shared/config/config.ts'\nimport { PrerenderReady } from '@/shared/ui/prerender-ready.tsx'\n\nimport { App } from '../App.tsx'\n\nexport type MutationMeta = {\n  /**\n   * Если 'off' — глобальный обработчик ошибок пропускаем,\n   * если 'on' (или нет поля) — вызываем.\n   */\n  globalErrorHandler?: 'on' | 'off'\n}\n\ndeclare module '@tanstack/react-query' {\n  interface Register {\n    /**\n     * Тип для поля `meta` в useMutation(...)\n     */\n    mutationMeta: MutationMeta\n  }\n}\n\nsetClientConfig({\n  baseURL: API_BASE_URL,\n  apiKey: API_KEY,\n  getAccessToken: async () => localStorage.getItem(localStorageKeys.accessToken),\n  getRefreshToken: async () => localStorage.getItem(localStorageKeys.refreshToken),\n  saveAccessToken: async (token) =>\n    token\n      ? localStorage.setItem(localStorageKeys.accessToken, token)\n      : localStorage.removeItem(localStorageKeys.accessToken),\n  saveRefreshToken: async (token) =>\n    token\n      ? localStorage.setItem(localStorageKeys.refreshToken, token)\n      : localStorage.removeItem(localStorageKeys.refreshToken),\n\n  toManyRequestsErrorHandler: (message: string | null) => {\n    toast(message)\n  },\n  logoutHandler: () => {\n    // store.dispatch(logoutThunk())\n  },\n})\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <QueryClientProvider client={queryClient}>\n      <BrowserRouter basename=\"/reatom\">\n        <App />\n        <PrerenderReady />\n      </BrowserRouter>\n    </QueryClientProvider>\n  </StrictMode>\n)\n"
  },
  {
    "path": "apps/reatom/src/app/query-client/query-client.tsx",
    "content": "import { MutationCache, QueryClient } from '@tanstack/react-query'\n\nimport { mutationGlobalErrorHandler } from '@/shared/ui/utils/query-error-handler-for-rhf-factory.ts'\n\nexport const queryClient = new QueryClient({\n  mutationCache: new MutationCache({\n    onError: mutationGlobalErrorHandler, // 🔹 вызывается ВСЕГДА\n  }),\n  defaultOptions: {\n    queries: {\n      refetchOnWindowFocus: false,\n      refetchOnMount: false,\n      staleTime: Infinity, //5000,\n      //gcTime: 10000 // если нет подписчиков - удалить всё нафик...\n    },\n  },\n})\n"
  },
  {
    "path": "apps/reatom/src/app/routing/Routing.tsx",
    "content": "import { Route, Routes } from 'react-router'\n\nimport { Layout } from '@/layout'\nimport { MainPage, PlaylistPage, PlaylistsPage, TrackPage, TracksPage, UserPage } from '@/pages'\nimport { OAuthCallback } from '@/pages/auth/OAuthRedirect/OAuthCallback.tsx'\n\nexport const Routing = () => (\n  <Routes>\n    <Route path=\"/oauth/callback\" element={<OAuthCallback />} />\n    <Route path=\"/\" element={<Layout />}>\n      <Route index element={<MainPage />} />\n\n      <Route path=\"/tracks\" element={<TracksPage />} />\n      <Route path=\"/tracks/:id\" element={<TrackPage />} />\n\n      <Route path=\"/playlists\" element={<PlaylistsPage />} />\n      <Route path=\"/playlists/:id\" element={<PlaylistPage />} />\n\n      <Route path=\"/user/:id\" element={<UserPage />} />\n    </Route>\n  </Routes>\n)\n"
  },
  {
    "path": "apps/reatom/src/app/routing/index.ts",
    "content": "export { Routing } from './Routing'\n"
  },
  {
    "path": "apps/reatom/src/app/styles/fonts.css",
    "content": "/*\n  source: https://gwfh.mranftl.com/fonts/lato?subsets=latin\n*/\n\n/* lato-regular - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-700 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-900 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 900;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n"
  },
  {
    "path": "apps/reatom/src/app/styles/global.css",
    "content": ":root {\n  font-family: Lato, sans-serif;\n  font-weight: 400;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  line-height: 100%;\n  text-rendering: optimizelegibility;\n\n  font-synthesis: none;\n}\n\n/* Scrollbar styles */\n* {\n  scrollbar-color: var(--color-bg-secondary) var(--color-bg-primary);\n  scrollbar-width: thin;\n}\n\nbody {\n  margin: 0;\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-primary);\n}\n"
  },
  {
    "path": "apps/reatom/src/app/styles/reset.css",
    "content": "/* Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset */\n\n/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Prevent font size inflation */\nhtml {\n  text-size-adjust: none;\n}\n\n/* Remove default margin in favour of better control in authored CSS */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nfigure,\nblockquote,\ndl,\ndd {\n  margin-block-end: 0;\n}\n\nul,\nol {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  line-height: 1.5;\n}\n\n/* Set shorter line heights on headings and interactive elements */\nh1,\nh2,\nh3,\nh4,\nbutton,\ninput,\nlabel {\n  border: none;\n  line-height: 1.1;\n}\n\n/* Balance text wrapping on headings */\nh1,\nh2,\nh3,\nh4 {\n  text-wrap: balance;\n}\n\n/* A elements that don't have a class get default styles */\na {\n  color: currentcolor;\n  text-decoration: none;\n}\n\n/* Make images easier to work with */\nimg,\npicture {\n  display: block;\n  max-width: 100%;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n/* Anything that has been anchored to should have extra scroll margin */\n:target {\n  scroll-margin-block: 5ex;\n}\n"
  },
  {
    "path": "apps/reatom/src/app/styles/variables.css",
    "content": ":root {\n  /*\n  * Colors\n  */\n  --color-accent: #ff38b6;\n  --color-disabled: #858585;\n  --color-outline-focus: #1a75f5;\n\n  /* Text */\n  --color-text-primary: #fff;\n  --color-text-primary-reverse: #000;\n  --color-text-secondary: #b3b3b3;\n  --color-text-label: #808080;\n  --color-text-error: #f51a51;\n\n  /* Backgrounds */\n  --color-bg-primary: #000;\n  --color-bg-secondary: #141414;\n  --color-bg-primary-reverse: #fff;\n  --color-bg-input-hover: #262626;\n  --color-bg-card: rgb(7 7 7 / 50%);\n  --color-bg-interactive-secondary: #333;\n\n  /* Borders */\n  --color-border-base: #7f7f7f;\n  --color-border-input-primary: #4d4d4d;\n  --color-border-input-active: #fffefe;\n\n  /*\n  * Typography\n  */\n\n  /* font-sizes */\n  --font-size-xxxs: 12px;\n  --font-size-xxs: 13px;\n  --font-size-xs: 14px;\n  --font-size-s: 16px;\n  --font-size-m: 18px;\n  --font-size-l: 20px;\n  --font-size-xl: 24px;\n  --font-size-xxl: 30px;\n  --font-size-xxxl: 60px;\n\n  /*\n  * Layout\n  */\n  --header-height: 80px;\n  --player-height: 112px;\n}\n"
  },
  {
    "path": "apps/reatom/src/entities/playlist/index.tsx",
    "content": "export { PlaylistCard } from './ui/PlaylistCard'\nexport { PlaylistItem } from './ui/PlaylistItem'\n"
  },
  {
    "path": "apps/reatom/src/entities/playlist/ui/PlaylistCard/PlaylistCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n  width: 264px;\n}\n\n.image {\n  overflow: hidden;\n  height: 153px;\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.description {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/reatom/src/entities/playlist/ui/PlaylistCard/PlaylistCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ImageSizeType, ReactionValue } from '@/shared/api/schema.ts'\n\nimport { CurrentUserReaction } from '../../../../features/playlists/api'\nimport { PlaylistCard } from './PlaylistCard.tsx'\n\nconst meta: Meta<typeof PlaylistCard> = {\n  title: 'entities/PlaylistCard',\n  component: PlaylistCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistCard>\n\nexport const Default: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    images: {\n      main: [\n        {\n          url: 'https://unsplash.it/182/182',\n          fileSize: 12,\n          width: 1212,\n          height: 2323,\n          type: ImageSizeType.medium,\n        },\n      ],\n    },\n    description: 'A playlist for relaxing and unwinding.',\n  },\n}\n\nexport const WithReactions: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    images: {\n      main: [\n        {\n          url: 'https://unsplash.it/182/182',\n          fileSize: 12,\n          width: 1212,\n          height: 2323,\n          type: ImageSizeType.medium,\n        },\n      ],\n    },\n    description: 'A playlist for relaxing and unwinding.',\n    render: () => <span>Like</span>,\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    id: '1',\n    title: 'The Best Hits of Elton John',\n    images: {\n      main: [\n        {\n          url: 'https://unsplash.it/182/182',\n          fileSize: 12,\n          width: 1212,\n          height: 2323,\n          type: ImageSizeType.medium,\n        },\n      ],\n    },\n    description:\n      'A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding.',\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/entities/playlist/ui/PlaylistCard/PlaylistCard.tsx",
    "content": "import type { FC, ReactNode } from 'react'\nimport { Link } from 'react-router'\n\nimport type { SchemaPlaylistImagesOutputDto } from '@/shared/api/schema.ts'\nimport { Card, Typography } from '@/shared/components'\nimport { VU } from '@/shared/utils'\n\nimport stab from '../../../../assets/img/no-cover.png'\nimport s from './PlaylistCard.module.css'\n\ntype PlaylistCardPropsBase = {\n  id: string\n  title: string\n  images: SchemaPlaylistImagesOutputDto\n  description: string | null\n}\n\ntype PlaylistCardProps = PlaylistCardPropsBase & {\n  render?: () => ReactNode\n}\n\nexport const PlaylistCard: FC<PlaylistCardProps> = (props) => {\n  const { title, images, description, id, render } = props\n\n  let imageSrc = images?.main?.length ? images.main[0].url : undefined\n\n  if (!imageSrc) {\n    imageSrc = stab\n  }\n\n  return (\n    <Card as={Link} to={`/playlists/${id}`} className={s.card}>\n      <div className={s.image}>\n        <img src={imageSrc} alt=\"\" aria-hidden />\n      </div>\n      <Typography variant=\"h3\" className={s.title}>\n        {title}\n      </Typography>\n      <Typography variant=\"body3\" className={s.description}>\n        {description}\n      </Typography>\n      {VU.isFunction(render) && render()}\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/entities/playlist/ui/PlaylistCard/index.ts",
    "content": "export * from './PlaylistCard.tsx'\n"
  },
  {
    "path": "apps/reatom/src/entities/playlist/ui/PlaylistItem/PlaylistItem.tsx",
    "content": "import type { FC } from 'react'\n\nimport type { PlaylistItemProps } from '@/entities/playlist/ui/PlaylistItem/PlaylistItem.types.ts'\nimport { usePlaylistReactions } from '@/features/playlists/api/use-playlists.query.ts'\nimport { ReactionProvider } from '@/features/reactions/ui'\nimport { ReactionButtons } from '@/shared/components'\n\nimport { PlaylistCard } from '../PlaylistCard'\n\nexport const PlaylistItem: FC<PlaylistItemProps> = (props) => {\n  const { playlist } = props\n\n  const { currentUserReaction, title, images, description, likesCount } = playlist.attributes\n  const { handleLike, handleDislike, handleRemoveReaction } = usePlaylistReactions(playlist.id)\n\n  return (\n    <PlaylistCard\n      id={playlist.id}\n      title={title}\n      images={images}\n      description={description}\n      render={() => (\n        <ReactionProvider\n          entityId={playlist.id}\n          currentReaction={currentUserReaction}\n          onLike={() => handleLike()}\n          onDislike={() => handleDislike()}\n          onRemoveReaction={() => handleRemoveReaction()}\n          likesCount={likesCount}>\n          {({ reaction, onLike, onDislike, likesCount }) => (\n            <ReactionButtons\n              reaction={reaction}\n              onLike={onLike}\n              onDislike={onDislike}\n              likesCount={likesCount}\n            />\n          )}\n        </ReactionProvider>\n      )}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/entities/playlist/ui/PlaylistItem/PlaylistItem.types.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\n// duplication of the CurrentUserReaction type to decouple the shared layer from the features layer\nexport type CurrentUserReaction = components['schemas']['ReactionValue']\n\nexport interface PlaylistItemProps {\n  playlist: components['schemas']['PlaylistListItemJsonApiData']\n}\n"
  },
  {
    "path": "apps/reatom/src/entities/playlist/ui/PlaylistItem/index.ts",
    "content": "export { PlaylistItem } from './PlaylistItem.tsx'\n"
  },
  {
    "path": "apps/reatom/src/features/artists/api/artists-api.ts",
    "content": "export const MOCK_ARTISTS = [\n  {\n    id: '1',\n    name: 'Kanye West',\n    image: 'https://unsplash.it/148/148',\n  },\n  {\n    id: '2',\n    name: 'Drake & The Weeknd & Kanye West',\n    image: 'https://unsplash.it/149/149',\n  },\n  {\n    id: '3',\n    name: 'Frank Ocean',\n    image: 'https://unsplash.it/150/150',\n  },\n  {\n    id: '4',\n    name: 'Headlund',\n    image: 'https://unsplash.it/151/151',\n  },\n  {\n    id: '5',\n    name: 'Rihanna',\n    image: 'https://unsplash.it/152/152',\n  },\n  {\n    id: '6',\n    name: 'Lamar',\n    image: 'https://unsplash.it/153/153',\n  },\n  {\n    id: '7',\n    name: 'The Weeknd',\n    image: 'https://unsplash.it/154/154',\n  },\n  {\n    id: '8',\n    name: 'Kendrick Lamar',\n    image: 'https://unsplash.it/155/155',\n  },\n  {\n    id: '9',\n    name: 'J. Cole',\n    image: 'https://unsplash.it/156/156',\n  },\n  {\n    id: '10',\n    name: 'Lil Uzi Vert',\n    image: 'https://unsplash.it/157/157',\n  },\n]\n"
  },
  {
    "path": "apps/reatom/src/features/artists/api/index.ts",
    "content": ""
  },
  {
    "path": "apps/reatom/src/features/artists/index.ts",
    "content": ""
  },
  {
    "path": "apps/reatom/src/features/artists/ui/ArtistCard/ArtistCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n\n  width: 148px;\n  height: 180px;\n}\n\n.image {\n  overflow: hidden;\n\n  width: 148px;\n  height: 148px;\n  border-radius: 50%;\n\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-align: center;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/artists/ui/ArtistCard/ArtistCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ArtistCard } from './ArtistCard'\n\nconst meta: Meta<typeof ArtistCard> = {\n  title: 'entities/ArtistCard',\n  component: ArtistCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof ArtistCard>\n\nexport const Default: Story = {\n  args: {\n    image: 'https://unsplash.it/182/182',\n    name: 'Kanye West',\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    image: 'https://unsplash.it/183/183',\n    name: 'Drake & The Weeknd & Kanye West',\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/features/artists/ui/ArtistCard/ArtistCard.tsx",
    "content": "import { Typography } from '@/shared/components'\n\nimport s from './ArtistCard.module.css'\n\ntype Props = {\n  image: string\n  name: string\n}\n\nexport const ArtistCard = ({ image, name }: Props) => {\n  return (\n    <div className={s.card}>\n      <div className={s.image}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <Typography variant=\"h3\" className={s.title}>\n        {name}\n      </Typography>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/artists/ui/ArtistCard/index.ts",
    "content": "export * from './ArtistCard'\n"
  },
  {
    "path": "apps/reatom/src/features/auth/api/use-login.mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\n\nimport { localStorageKeys, type LoginRequestPayload } from '../types/auth-api.types'\n\nexport const useLoginMutation = () => {\n  const qc = useQueryClient()\n  return useMutation({\n    mutationFn: (payload: LoginRequestPayload) => {\n      return getClient().POST('/auth/login', {\n        body: payload,\n      })\n    },\n    onSuccess: async (data) => {\n      localStorage.setItem(localStorageKeys.refreshToken, data.data!.refreshToken)\n      localStorage.setItem(localStorageKeys.accessToken, data.data!.accessToken)\n      await qc.invalidateQueries({ queryKey: ['auth', 'me'] })\n\n      await qc.invalidateQueries()\n    },\n  })\n}\n"
  },
  {
    "path": "apps/reatom/src/features/auth/api/use-logout.mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\nimport { requestWrapper } from '@/shared/api/utils/request-wrapper.ts'\n\nimport { localStorageKeys } from '../types/auth-api.types'\n\nexport const useLogoutMutation = () => {\n  const qc = useQueryClient()\n  return useMutation({\n    mutationFn: () => {\n      return requestWrapper(\n        getClient().POST('/auth/logout', {\n          body: {\n            refreshToken: localStorage.getItem(localStorageKeys.refreshToken)!,\n          },\n        })\n      )\n    },\n    onSuccess: async () => {\n      localStorage.removeItem(localStorageKeys.accessToken)\n      localStorage.removeItem(localStorageKeys.refreshToken)\n      await qc.resetQueries({ queryKey: ['auth', 'me'] }) // resetQueries переводит query в изначальное состояние и уведомляет подписчиков — компонент получит data = undefined.\n      //await qc.invalidateQueries({ queryKey: ['auth', 'me'] }) // invalidateQueries заставит его немедленно перефетчиться без токена ⇒ получите 401 ⇒ data станет undefined / error.\n    },\n  })\n}\n"
  },
  {
    "path": "apps/reatom/src/features/auth/api/use-me.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\nimport { requestWrapper } from '@/shared/api/utils/request-wrapper.ts'\n\nexport const useMeQuery = () => {\n  return useQuery({\n    queryKey: ['auth', 'me'],\n    queryFn: () => requestWrapper(getClient().GET('/auth/me')),\n  })\n}\n"
  },
  {
    "path": "apps/reatom/src/features/auth/index.ts",
    "content": "export * from './ui'\n"
  },
  {
    "path": "apps/reatom/src/features/auth/types/auth-api.types.ts",
    "content": "import { getClientConfig } from '@/shared/api/client.ts'\nimport type { components } from '@/shared/api/schema.ts'\n\nexport type RefreshOutput = components['schemas']['RefreshOutput']\n\nexport type RefreshRequestPayload = components['schemas']['RefreshRequestPayload']\n\nexport type LoginRequestPayload = components['schemas']['LoginRequestPayload']\n\nexport const localStorageKeys = {\n  refreshToken: 'spotifun-refresh-token',\n  accessToken: 'spotifun-access-token',\n}\n\nexport const getOauthRedirectUrl = (redirectUrl: string) =>\n  getClientConfig().baseURL + `/auth/oauth-redirect?callbackUrl=${redirectUrl}`\n"
  },
  {
    "path": "apps/reatom/src/features/auth/ui/LoginButtonAndModal/LoginButtonAndModal.module.css",
    "content": ".dialog {\n  width: 376px;\n  padding-bottom: 22px;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n  align-items: center;\n\n  text-align: center;\n}\n\n.icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 60px;\n  height: 60px;\n  border-radius: 50%;\n\n  font-size: 24px;\n\n  background-color: var(--color-accent);\n}\n\n.button {\n  height: 55px;\n}\n\n.secondary {\n  background-color: #555;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/auth/ui/LoginButtonAndModal/LoginButtonAndModal.tsx",
    "content": "import clsx from 'clsx'\nimport { useState } from 'react'\n\nimport { useLoginMutation } from '@/features/auth/api/use-login.mutation.ts'\nimport { getOauthRedirectUrl } from '@/features/auth/types/auth-api.types.ts'\nimport { Button } from '@/shared/components/Button'\nimport { Dialog, DialogContent, DialogHeader } from '@/shared/components/Dialog'\nimport { Typography } from '@/shared/components/Typography'\nimport { CURRENT_APP_DOMAIN } from '@/shared/config/config.ts'\n\nimport s from './LoginButtonAndModal.module.css'\n\nexport const LoginButtonAndModal = () => {\n  const [isOpen, setIsOpen] = useState(false)\n\n  const handleOpenModal = () => setIsOpen(true)\n  const handleCloseModal = () => setIsOpen(false)\n\n  const { mutate } = useLoginMutation()\n\n  const loginHandler = () => {\n    const redirectUri = window.location.origin + CURRENT_APP_DOMAIN + 'oauth/callback' // todo: to config\n    const url = getOauthRedirectUrl(redirectUri)\n    window.open(url, 'oauthPopup', 'width=500,height=600')\n\n    const receiveMessage = async (event: MessageEvent) => {\n      if (event.origin !== window.location.origin) {\n        // todo: to config\n        return\n      }\n\n      const { code } = event.data\n      if (code) {\n        console.log('✅ code received:', code)\n        // тут можно вызвать setToken(accessToken) или dispatch(login)\n        //popup?.close()\n        window.removeEventListener('message', receiveMessage)\n        mutate({ code, accessTokenTTL: '10s', redirectUri, rememberMe: true })\n        handleCloseModal()\n      }\n    }\n\n    window.addEventListener('message', receiveMessage)\n  }\n\n  return (\n    <>\n      <Button variant=\"primary\" onClick={handleOpenModal}>\n        Sign in\n      </Button>\n\n      <Dialog open={isOpen} onClose={handleCloseModal} className={s.dialog}>\n        <DialogHeader />\n\n        <DialogContent className={s.content}>\n          <Typography variant=\"h2\">\n            Millions of Songs. <br /> Free on Musicfun.\n          </Typography>\n\n          <div className={s.icon}>😊</div>\n\n          <Button className={clsx(s.button, s.secondary)} fullWidth onClick={handleCloseModal}>\n            Continue without Sign in\n          </Button>\n          <Button\n            as=\"button\"\n            target=\"_blank\"\n            className={s.button}\n            variant=\"primary\"\n            fullWidth\n            onClick={loginHandler}>\n            Sign in with APIHub\n          </Button>\n        </DialogContent>\n      </Dialog>\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/auth/ui/LoginButtonAndModal/index.ts",
    "content": "export { LoginButtonAndModal } from './LoginButtonAndModal'\n"
  },
  {
    "path": "apps/reatom/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.module.css",
    "content": ".trigger {\n  cursor: pointer;\n  display: flex;\n  gap: 8px;\n  align-items: center;\n}\n\n.avatar {\n  overflow: hidden;\n  width: 34px;\n  height: 34px;\n  border-radius: 50%;\n}\n\n.name {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/reatom/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ProfileDropdownMenu } from './ProfileDropdownMenu'\n\nconst meta: Meta<typeof ProfileDropdownMenu> = {\n  title: 'entities/ProfileDropdownMenu',\n  component: ProfileDropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof ProfileDropdownMenu>\n\nexport const Default: Story = {\n  args: {\n    avatar: 'https://unsplash.it/182/182',\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.tsx",
    "content": "import { Link } from 'react-router'\n\nimport { useLogoutMutation } from '@/features/auth/api/use-logout.mutation.ts'\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Typography,\n} from '@/shared/components'\nimport { LogoutIcon, ProfileIcon } from '@/shared/icons'\n\nimport s from './ProfileDropdownMenu.module.css'\n\nexport const ProfileDropdownMenu = ({ avatar }: { avatar: string }) => {\n  const { data } = useMeQuery()\n  const logoutMutation = useLogoutMutation()\n\n  const handleLogout = () => {\n    logoutMutation.mutate()\n  }\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild className={s.trigger}>\n        <div className={s.avatar}>\n          <img src={avatar} alt={''} />\n        </div>\n        <Typography className={s.name} variant=\"body2\">\n          {data!.login}\n        </Typography>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem as={Link} to={`/user/${data!.userId}`}>\n          <ProfileIcon />\n          <span>My Profile</span>\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={handleLogout} disabled={logoutMutation.isPending}>\n          <LogoutIcon />\n          <span>{logoutMutation.isPending ? 'Logging out...' : 'Logout'}</span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/auth/ui/ProfileDropdownMenu/index.ts",
    "content": "export * from './ProfileDropdownMenu'\n"
  },
  {
    "path": "apps/reatom/src/features/auth/ui/index.ts",
    "content": "export * from './LoginButtonAndModal'\nexport * from './ProfileDropdownMenu'\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/api/index.ts",
    "content": "export * from './playlistsApi'\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/api/playlistsApi.ts",
    "content": "import { getClient } from '@/shared/api/client.ts'\nimport type { SchemaReactionOutput } from '@/shared/api/schema.ts'\nimport { ImageSizeType, ReactionValue, type SchemaGetPlaylistOutput } from '@/shared/api/schema.ts'\n\nexport const playlistsApi = {\n  likePlaylist: (playlistId: SchemaReactionOutput['objectId']) => {\n    return getClient().POST('/playlists/{playlistId}/likes', {\n      params: {\n        path: {\n          playlistId,\n        },\n      },\n    })\n  },\n\n  dislikePlaylist: (playlistId: SchemaReactionOutput['objectId']) => {\n    return getClient().POST('/playlists/{playlistId}/dislikes', {\n      params: {\n        path: {\n          playlistId,\n        },\n      },\n    })\n  },\n\n  removePlaylistReaction: (playlistId: SchemaReactionOutput['objectId']) => {\n    return getClient().DELETE('/playlists/{playlistId}/reactions', {\n      params: {\n        path: {\n          playlistId,\n        },\n      },\n    })\n  },\n}\n\nexport enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = 2,\n}\n\nexport const MOCK_PLAYLISTS: SchemaGetPlaylistOutput[] = [\n  {\n    data: {\n      id: '1',\n      type: 'playlists',\n      attributes: {\n        title: 'Chill Vibes',\n        description: 'Relax and unwind with these chill tracks 🌊',\n        addedAt: '2025-06-01T12:00:00Z',\n        updatedAt: '2025-06-10T15:30:00Z',\n        order: 1,\n        user: { id: 'user-101', name: 'Alice' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 640,\n              height: 640,\n              fileSize: 204800,\n              url: 'https://unsplash.it/183/183',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'chill' },\n          { id: '2', name: 'lofi' },\n          { id: '3', name: 'relax' },\n        ],\n        currentUserReaction: ReactionValue.Value1,\n        likesCount: 542,\n        dislikesCount: 2,\n      },\n    },\n  },\n  {\n    data: {\n      id: '2',\n      type: 'playlists',\n      attributes: {\n        title: 'Workout Pump',\n        description: 'High energy tracks to keep you moving 💪',\n        addedAt: '2025-05-20T08:00:00Z',\n        updatedAt: '2025-06-05T18:00:00Z',\n        order: 2,\n        user: { id: 'user-202', name: 'Bob' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 800,\n              height: 800,\n              fileSize: 307200,\n              url: 'https://unsplash.it/184/184',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'fitness' },\n          { id: '2', name: 'pump' },\n          { id: '3', name: 'motivation' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 123,\n        dislikesCount: 9,\n      },\n    },\n  },\n  {\n    data: {\n      id: '3',\n      type: 'playlists',\n      attributes: {\n        title: 'Fantasy Soundtrack',\n        description: 'Epic and magical music for your quests 🏹',\n        addedAt: '2025-04-15T14:30:00Z',\n        updatedAt: '2025-05-01T10:10:00Z',\n        order: 3,\n        user: { id: 'user-303', name: 'Elrond' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 1024,\n              height: 768,\n              fileSize: 512000,\n              url: 'https://unsplash.it/185/185',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'fantasy' },\n          { id: '2', name: 'soundtrack' },\n          { id: '3', name: 'epic' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 54,\n        dislikesCount: 7,\n      },\n    },\n  },\n  {\n    data: {\n      id: '4',\n      type: 'playlists',\n      attributes: {\n        title: 'Suffer possible assume',\n        description: 'Recently religious responsibility whether only.',\n        addedAt: '2025-04-29T10:39:13',\n        updatedAt: '2025-06-14T21:01:35',\n        order: 4,\n        user: { id: 'user-4', name: 'Katie' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 936,\n              height: 306,\n              fileSize: 243840,\n              url: 'https://unsplash.it/192/192',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'any' },\n          { id: '2', name: 'shake' },\n          { id: '3', name: 'white' },\n        ],\n        currentUserReaction: ReactionValue.Value1,\n        likesCount: 3,\n        dislikesCount: 4,\n      },\n    },\n  },\n  {\n    data: {\n      id: '5',\n      type: 'playlists',\n      attributes: {\n        title: 'Risk still',\n        description: 'Skin pay sure yeah couple live heart.',\n        addedAt: '2025-01-26T00:52:16',\n        updatedAt: '2025-06-14T21:00:56',\n        order: 5,\n        user: { id: 'user-5', name: 'Robert' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 525,\n              height: 500,\n              fileSize: 185000,\n              url: 'https://unsplash.it/191/191',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'term' },\n          { id: '2', name: 'item' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 14,\n        dislikesCount: 12,\n      },\n    },\n  },\n  {\n    data: {\n      id: '6',\n      type: 'playlists',\n      attributes: {\n        title: 'Attack through go',\n        description: 'Plan deep sport growth tonight.',\n        addedAt: '2025-04-07T10:16:19',\n        updatedAt: '2025-06-14T21:02:28',\n        order: 6,\n        user: { id: 'user-6', name: 'Shelly' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 985,\n              height: 44,\n              fileSize: 105000,\n              url: 'https://unsplash.it/190/190',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'feeling' },\n          { id: '2', name: 'size' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 0,\n        dislikesCount: 2,\n      },\n    },\n  },\n  {\n    data: {\n      id: '7',\n      type: 'playlists',\n      attributes: {\n        title: 'Yet woman outside',\n        description: 'Attorney especially child music capital well.',\n        addedAt: '2025-01-02T16:37:47',\n        updatedAt: '2025-06-14T21:03:26',\n        order: 7,\n        user: { id: 'user-7', name: 'Kristopher' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 541,\n              height: 589,\n              fileSize: 312000,\n              url: 'https://unsplash.it/189/189',\n            },\n          ],\n        },\n        tags: [{ id: '1', name: 'week' }],\n        currentUserReaction: ReactionValue.Value1,\n        likesCount: 12,\n        dislikesCount: 1,\n      },\n    },\n  },\n  {\n    data: {\n      id: '8',\n      type: 'playlists',\n      attributes: {\n        title: 'Community',\n        description: 'Visit about occur it fast industry process.',\n        addedAt: '2025-06-03T22:12:23',\n        updatedAt: '2025-06-14T21:00:31',\n        order: 8,\n        user: { id: 'user-8', name: 'Kimberly' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 376,\n              height: 803,\n              fileSize: 460000,\n              url: 'https://unsplash.it/188/188',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'serve' },\n          { id: '2', name: 'although' },\n          { id: '3', name: 'item' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 12,\n        dislikesCount: 14,\n      },\n    },\n  },\n  {\n    data: {\n      id: '9',\n      type: 'playlists',\n      attributes: {\n        title: 'Dance Lights Forever',\n        description: 'Feel the beat drop and the lights flash 🎉',\n        addedAt: '2024-12-14T15:20:12',\n        updatedAt: '2025-06-13T17:15:00',\n        order: 9,\n        user: { id: 'user-9', name: 'Jasmine' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 800,\n              height: 800,\n              fileSize: 310000,\n              url: 'https://unsplash.it/187/187',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'dance' },\n          { id: '2', name: 'party' },\n          { id: '3', name: 'electro' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 2,\n        dislikesCount: 14,\n      },\n    },\n  },\n  {\n    data: {\n      id: '10',\n      type: 'playlists',\n      attributes: {\n        title: 'Calm Forest Ambience',\n        description: 'Let nature help you concentrate 🌲',\n        addedAt: '2025-03-01T09:45:00',\n        updatedAt: '2025-06-10T13:20:00',\n        order: 10,\n        user: { id: 'user-10', name: 'Leo' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 1024,\n              height: 576,\n              fileSize: 280000,\n              url: 'https://unsplash.it/186/186',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'nature' },\n          { id: '2', name: 'focus' },\n          { id: '3', name: 'relax' },\n        ],\n        currentUserReaction: ReactionValue.ValueMinus1,\n        likesCount: 84,\n        dislikesCount: 14,\n      },\n    },\n  },\n]\n\nexport const MOCK_PLAYLIST = {\n  data: {\n    id: '10',\n    type: 'playlists',\n    attributes: {\n      title: 'Calm Forest Ambience',\n      description: {\n        text: 'Let nature help you concentrate 🌲',\n      },\n      addedAt: '2025-03-01T09:45:00',\n      updatedAt: '2025-06-10T13:20:00',\n      order: 10,\n      user: {\n        id: 'user-10',\n        name: 'Leo',\n      },\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 1024,\n            height: 576,\n            fileSize: 280000,\n            url: 'https://unsplash.it/300/300',\n          },\n        ],\n      },\n      tags: ['nature', 'focus', 'relax'],\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 12,\n    },\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/api/query-key-factory.ts",
    "content": "import type { SchemaGetPlaylistsRequestPayload } from '@/shared/api/schema.ts'\n\nexport const playlistsKeys = {\n  all: ['playlists'] as const, // playlists\n  lists: () => [...playlistsKeys.all, 'list'] as const, //  playlists, list\n  list: (filters: SchemaGetPlaylistsRequestPayload) =>\n    [...playlistsKeys.lists(), { filters }] as const, //  playlists, list, {:filter}\n  details: () => [...playlistsKeys.all, 'detail'] as const, // playlists, detail\n  detail: (id: number) => [...playlistsKeys.details(), id] as const, // playlists, details, :id\n}\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/api/types.ts",
    "content": ""
  },
  {
    "path": "apps/reatom/src/features/playlists/api/use-playlists.query.ts",
    "content": "import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\n\nimport { playlistsApi } from '@/features/playlists/api/playlistsApi.ts'\nimport { playlistsKeys } from '@/features/playlists/api/query-key-factory.ts'\nimport { getClient } from '@/shared/api/client.ts'\nimport type { SchemaGetPlaylistsRequestPayload, SchemaReactionOutput } from '@/shared/api/schema.ts'\nimport { VU } from '@/shared/utils'\n\nexport const usePlaylists = ({\n  search,\n  pageNumber,\n  pageSize,\n  userId,\n  sortBy,\n  sortDirection,\n  tagsIds,\n  trackId,\n}: SchemaGetPlaylistsRequestPayload) => {\n  const query = useQuery({\n    queryKey: playlistsKeys.list({\n      search,\n      pageNumber,\n      pageSize,\n      userId,\n      sortBy,\n      sortDirection,\n      tagsIds,\n      trackId,\n    }),\n    queryFn: () => {\n      return getClient().GET('/playlists', {\n        params: {\n          query: {\n            search: search || void 0,\n            pageNumber,\n            pageSize,\n            userId,\n            sortBy,\n            sortDirection,\n            tagsIds: VU.isValid(tagsIds) ? tagsIds : void 0,\n            trackId,\n          },\n        },\n      })\n    },\n    placeholderData: keepPreviousData,\n  })\n\n  return query\n}\n\nexport const useLikePlaylist = () => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: playlistsApi.likePlaylist,\n    onSuccess: () => {\n      // Invalidate all playlist queries to refresh data\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n    },\n    onError: (error) => {\n      console.error('Failed to like playlist:', error)\n      // Here you can add user notification about the error\n    },\n  })\n}\n\nexport const useDislikePlaylist = () => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: playlistsApi.dislikePlaylist,\n    onSuccess: () => {\n      // Invalidate all playlist queries to refresh data\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n    },\n    onError: (error) => {\n      console.error('Failed to dislike playlist:', error)\n      // Here you can add user notification about the error\n    },\n  })\n}\n\nexport const useRemovePlaylistReaction = () => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: playlistsApi.removePlaylistReaction,\n    onSuccess: () => {\n      // Invalidate all playlist queries to refresh data\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n    },\n    onError: (error) => {\n      console.error('Failed to remove playlist reaction:', error)\n      // Here you can add user notification about the error\n    },\n  })\n}\n\n// Hook for managing playlist reactions\nexport const usePlaylistReactions = (playlistId: SchemaReactionOutput['objectId']) => {\n  const likeMutation = useLikePlaylist()\n  const dislikeMutation = useDislikePlaylist()\n  const removeMutation = useRemovePlaylistReaction()\n\n  const handleLike = () => {\n    likeMutation.mutate(playlistId)\n  }\n\n  const handleDislike = () => {\n    dislikeMutation.mutate(playlistId)\n  }\n\n  const handleRemoveReaction = () => {\n    removeMutation.mutate(playlistId)\n  }\n\n  return {\n    handleLike,\n    handleDislike,\n    handleRemoveReaction,\n    isPending: likeMutation.isPending || dislikeMutation.isPending || removeMutation.isPending,\n  }\n}\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/model/model.tsx",
    "content": "import { atom } from '@reatom/core'\n\nimport { getClient } from '@/shared/api/client.ts'\nimport type { SchemaGetPlaylistsOutput } from '@/shared/api/schema.ts'\n\nexport const playlistsListAtom = atom(null as null | SchemaGetPlaylistsOutput, 'list')\n  .extend((target) => ({\n    // describe things that you want to assign to the current atom\n    isLoading: atom(false, `${target.name}.isLoading`),\n  }))\n  .actions((target) => ({\n    async load(page: number) {\n      target.isLoading.set(true)\n      const response = await getClient().GET(`/playlists`, {\n        params: {\n          query: {\n            pageNumber: page,\n          },\n        },\n      })\n      const payload = response.data!\n      target.set(payload)\n      target.isLoading.set(false)\n    },\n  }))\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/ui/CreatePlaylistModal/CreatePlaylistModal.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 745px;\n}\n\n.form {\n  overflow-y: auto;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n  margin-bottom: 16px;\n}\n\n.imageUploader {\n  width: 280px;\n  margin: 0 auto;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/ui/CreatePlaylistModal/CreatePlaylistModal.tsx",
    "content": "import { useState } from 'react'\n\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  ImageUploader,\n  TagEditor,\n  Textarea,\n  TextField,\n  Typography,\n} from '@/shared/components'\n\nimport s from './CreatePlaylistModal.module.css'\n\nexport const CreatePlaylistModal = ({ onClose }: { onClose: () => void }) => {\n  const [tags, setTags] = useState<string[]>([])\n  const handleTagsChange = (tags: string[]) => {\n    setTags(tags)\n  }\n\n  return (\n    <Dialog open onClose={onClose} className={s.dialog}>\n      <DialogHeader>\n        <Typography variant=\"h2\">Create Playlist</Typography>\n      </DialogHeader>\n\n      <form className={s.form}>\n        <DialogContent className={s.content}>\n          <ImageUploader className={s.imageUploader} onImageSelect={() => {}} />\n          <TextField label=\"Title\" placeholder=\"Enter playlist title\" />\n          <Textarea rows={3} label=\"Description\" placeholder=\"Enter playlist description\" />\n          <TagEditor label=\"Hashtags\" value={tags} onTagsChange={handleTagsChange} maxTags={5} />\n        </DialogContent>\n\n        <DialogFooter>\n          <Button variant=\"secondary\" onClick={onClose} type=\"button\">\n            Cancel\n          </Button>\n          <Button variant=\"primary\" type=\"submit\">\n            Create\n          </Button>\n        </DialogFooter>\n      </form>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/ui/CreatePlaylistModal/index.ts",
    "content": "export * from './CreatePlaylistModal'\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.module.css",
    "content": ".container {\n  display: flex;\n  gap: 24px;\n  background: transparent;\n}\n\n.imageContainer {\n  flex-shrink: 0;\n  width: 297px;\n  height: 297px;\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  min-width: 0;\n}\n\n.title {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  margin-bottom: 8px;\n\n  font-size: clamp(var(--font-size-xxl), 8vw, var(--font-size-xxxl));\n  font-weight: 900;\n  line-height: 1;\n  white-space: pre-wrap;\n}\n\n.description {\n  opacity: 0.7;\n}\n\n.info {\n  margin-top: auto;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_5_HASHTAGS } from '@/features/tags'\n\nimport { PlaylistOverview } from '../PlaylistOverview'\n\nconst meta: Meta<typeof PlaylistOverview> = {\n  title: 'entities/PlaylistOverview',\n  component: PlaylistOverview,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistOverview>\n\nexport const Default: Story = {\n  args: {\n    title: 'Chill Mix',\n    image: 'https://unsplash.it/297/297',\n    description: 'Julia Wolf, ayokay, Khalid and more',\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n\nexport const LongTitle: Story = {\n  args: {\n    title: 'This is a Very Long Playlist Title That Should Scale Responsively',\n    image: 'https://unsplash.it/299/299',\n    description: 'A collection of amazing tracks from various artists around the world',\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.tsx",
    "content": "import clsx from 'clsx'\nimport { type ComponentProps } from 'react'\n\nimport { TagsList } from '@/features/tags'\nimport { Typography } from '@/shared/components'\n\nimport s from './PlaylistOverview.module.css'\n\ntype PlaylistOverviewProps = {\n  title: string\n  image: string\n  description: string\n  tags: string[]\n} & ComponentProps<'div'>\n\nexport const PlaylistOverview = ({\n  title,\n  image,\n  description,\n  tags,\n  className,\n  ...props\n}: PlaylistOverviewProps) => {\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <div className={s.imageContainer}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <div className={s.content}>\n        <TagsList tags={tags} entity=\"playlists\" />\n\n        <Typography variant=\"h1\" as=\"h1\" className={s.title}>\n          {title}\n        </Typography>\n\n        <div className={s.info}>\n          <Typography variant=\"body1\" className={s.description}>\n            {description}\n          </Typography>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/ui/PlaylistOverview/index.ts",
    "content": "export * from './PlaylistOverview'\n"
  },
  {
    "path": "apps/reatom/src/features/playlists/ui/index.ts",
    "content": "export * from './CreatePlaylistModal'\nexport * from './PlaylistOverview'\n"
  },
  {
    "path": "apps/reatom/src/features/reactions/index.ts",
    "content": "export * from './ui'\n"
  },
  {
    "path": "apps/reatom/src/features/reactions/ui/ReactionProvider/ReactionProvider.tsx",
    "content": "import type { FC, ReactNode } from 'react'\nimport { useCallback } from 'react'\n\nexport type ReactionValue = 1 | -1 | 0\n\nexport interface ReactionProviderProps {\n  entityId: string\n  currentReaction?: ReactionValue\n  onLike: (entityId: string) => void\n  onDislike: (entityId: string) => void\n  onRemoveReaction: (entityId: string) => void\n  children: (props: {\n    reaction: ReactionValue\n    onLike: () => void\n    onDislike: () => void\n    likesCount?: number\n  }) => ReactNode\n  likesCount?: number\n}\n\nenum ReactionType {\n  Like = 1,\n  Dislike = -1,\n}\n\nexport const ReactionProvider: FC<ReactionProviderProps> = (props) => {\n  const {\n    entityId,\n    currentReaction = 0,\n    onLike,\n    onDislike,\n    onRemoveReaction,\n    children,\n    likesCount,\n  } = props\n\n  const toggleReaction = useCallback(\n    (reactionType: ReactionValue) => {\n      switch (true) {\n        case currentReaction === reactionType:\n          return onRemoveReaction(entityId)\n        case reactionType === ReactionType.Like:\n          return onLike(entityId)\n        case reactionType === ReactionType.Dislike:\n          return onDislike(entityId)\n        default:\n          return\n      }\n    },\n    [currentReaction, onDislike, onLike, onRemoveReaction, entityId]\n  )\n\n  const handleLike = useCallback(() => {\n    toggleReaction(ReactionType.Like)\n  }, [toggleReaction])\n\n  const handleDislike = useCallback(() => {\n    toggleReaction(ReactionType.Dislike)\n  }, [toggleReaction])\n\n  return (\n    <>\n      {children({\n        reaction: currentReaction,\n        onLike: handleLike,\n        onDislike: handleDislike,\n        likesCount,\n      })}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/reactions/ui/ReactionProvider/index.ts",
    "content": "export * from './ReactionProvider'\n"
  },
  {
    "path": "apps/reatom/src/features/reactions/ui/index.ts",
    "content": "export * from './ReactionProvider'\n"
  },
  {
    "path": "apps/reatom/src/features/tags/api/index.ts",
    "content": "export * from './tags-api'\n"
  },
  {
    "path": "apps/reatom/src/features/tags/api/tags-api.ts",
    "content": "export const MOCK_HASHTAGS = [\n  'Rock',\n  'Jazz',\n  'Blues',\n  'Metal',\n  'Folk',\n  'Coding',\n  'Dark Ambient',\n  'Chill',\n  'Lo-fi',\n]\n\nexport const MOCK_5_HASHTAGS = MOCK_HASHTAGS.slice(0, 5)\n\nexport type TagDto = {\n  id: string\n  name: string\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tags/api/use-tags.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\n\nexport const useTags = (search?: string) => {\n  return useQuery({\n    queryKey: ['tags', search],\n    queryFn: () => {\n      return getClient().GET('/tags/search', {\n        params: {\n          query: {\n            search: search || '',\n          },\n        },\n      })\n    },\n    enabled: true,\n  })\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tags/index.ts",
    "content": "export { MOCK_5_HASHTAGS, MOCK_HASHTAGS } from './api/tags-api'\nexport { useTags } from './api/use-tags.query'\nexport * from './ui'\n"
  },
  {
    "path": "apps/reatom/src/features/tags/ui/TagsList/TagsList.module.css",
    "content": ".list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tags/ui/TagsList/TagsList.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_HASHTAGS } from '../../api/tags-api'\nimport { TagsList } from './TagsList'\n\nconst meta: Meta<typeof TagsList> = {\n  title: 'entities/TagsList',\n  component: TagsList,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TagsList>\n\nexport const Default: Story = {\n  args: {\n    tags: MOCK_HASHTAGS,\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tags/ui/TagsList/TagsList.tsx",
    "content": "import { Link } from 'react-router'\n\nimport { Tag } from '@/shared/components'\n\nimport s from './TagsList.module.css'\n\nexport const TagsList = ({\n  tags,\n  entity = 'tracks',\n}: {\n  tags: string[]\n  entity?: 'tracks' | 'playlists'\n}) => {\n  return (\n    <ul className={s.list}>\n      {tags.map((tag) => (\n        <li key={tag}>\n          <Tag as={Link} to={`/${entity}?tag=${tag}`} tag={tag} />\n        </li>\n      ))}\n    </ul>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tags/ui/TagsList/index.ts",
    "content": "export { TagsList } from './TagsList'\n"
  },
  {
    "path": "apps/reatom/src/features/tags/ui/index.ts",
    "content": "export * from './'\nexport * from './TagsList'\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/api/index.ts",
    "content": "export * from './tracksApi'\nexport * from './types'\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/api/tracksApi.ts",
    "content": "import { ReactionValue, type SchemaReactionValue } from '@/shared/api/schema.ts'\n\nexport const MOCK_TRACKS = [\n  {\n    id: '1',\n    type: 'tracks',\n    attributes: {\n      artist: 'Headlund',\n      id: '1',\n      title: 'Days That Matter',\n      addedAt: '2025-06-01T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/110/110',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 104,\n      dislikesCount: 2,\n      artists: [{ id: '1', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '2',\n    type: 'tracks',\n    attributes: {\n      artist: 'Stellar Wave',\n      id: '2',\n      title: 'Cosmic Dust',\n      addedAt: '2025-06-02T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/111/111',\n          },\n        ],\n      },\n      user: {\n        id: '2',\n        name: 'Jane Smith',\n      },\n      currentUserReaction: ReactionValue.Value1,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '2', name: 'Jane Smith' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '3',\n    type: 'tracks',\n    attributes: {\n      artist: 'Aqua Marine',\n      id: '3',\n      title: 'Ocean Breath Is The Best Track Ever',\n      addedAt: '2025-06-03T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/112/112',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 1,\n      dislikesCount: 2,\n      artists: [\n        { id: '3', name: 'Peter Jones' },\n        { id: '4', name: 'Chris Green' },\n        { id: '5', name: 'John Doe' },\n      ],\n      duration: 100,\n    },\n  },\n  {\n    id: '4',\n    type: 'tracks',\n    attributes: {\n      artist: 'Night Rider',\n      id: '4',\n      title: 'Midnight Drive',\n      addedAt: '2025-06-04T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/113/113',\n          },\n        ],\n      },\n      user: {\n        id: '3',\n        name: 'Peter Jones',\n      },\n      currentUserReaction: ReactionValue.ValueMinus1,\n      likesCount: 666,\n      dislikesCount: 2,\n      artists: [{ id: '4', name: 'Chris Green' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '5',\n    type: 'tracks',\n    attributes: {\n      artist: 'Urban Glow',\n      id: '5',\n      title: 'City Lights',\n      addedAt: '2025-06-05T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/114/114',\n          },\n        ],\n      },\n      user: {\n        id: '2',\n        name: 'Jane Smith',\n      },\n      currentUserReaction: ReactionValue.Value1,\n      likesCount: 8,\n      dislikesCount: 2,\n      artists: [{ id: '5', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '6',\n    type: 'tracks',\n    attributes: {\n      artist: 'Whispering Pines',\n      id: '6',\n      title: 'Forest Lullaby',\n      addedAt: '2025-06-06T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/115/115',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 1,\n      dislikesCount: 2,\n      duration: 100,\n    },\n  },\n  {\n    id: '7',\n    type: 'tracks',\n    attributes: {\n      artist: 'Sandstorm',\n      id: '7',\n      title: 'Desert Mirage',\n      addedAt: '2025-06-07T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/116/116',\n          },\n        ],\n      },\n      user: {\n        id: '4',\n        name: 'Susan Lee',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '7', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '8',\n    type: 'tracks',\n    attributes: {\n      artist: 'Altitude',\n      id: '8',\n      title: 'Mountain Peak',\n      addedAt: '2025-06-08T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/117/117',\n          },\n        ],\n      },\n      user: {\n        id: '3',\n        name: 'Peter Jones',\n      },\n      currentUserReaction: ReactionValue.Value1,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '8', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '9',\n    type: 'tracks',\n    attributes: {\n      artist: 'Water Lily',\n      id: '9',\n      title: 'River Flow',\n      addedAt: '2025-06-09T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/118/118',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: ReactionValue.ValueMinus1,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '10', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '10',\n    type: 'tracks',\n    attributes: {\n      artist: 'Galaxy Explorer',\n      id: '10',\n      title: 'Final Frontier',\n      addedAt: '2025-06-10T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/119/119',\n          },\n        ],\n      },\n      user: {\n        id: '5',\n        name: 'Chris Green',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '10', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n]\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/api/types.ts",
    "content": "export enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = 2,\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackCard/TrackCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n  width: 128px;\n}\n\n.image {\n  overflow: hidden;\n  height: 103px;\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.artists {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  text-overflow: ellipsis;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackCard/TrackCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { TrackCard } from './TrackCard'\n\nconst meta: Meta<typeof TrackCard> = {\n  title: 'entities/TrackCard',\n  component: TrackCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TrackCard>\n\nexport const Default: Story = {\n  args: {\n    id: '1',\n    title: 'Name Song',\n    image: 'https://unsplash.it/182/182',\n    artists: 'Ed Sheeran, Big Sean, Juice W...',\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    id: '1',\n    title: 'A very long track title that should be truncated',\n    image: 'https://unsplash.it/183/183',\n    artists:\n      'A lot of artists on this track, so many that the text should overflow and be truncated by ellipsis',\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackCard/TrackCard.tsx",
    "content": "import { Link } from 'react-router'\n\nimport { Card, ReactionButtons, type ReactionButtonsProps, Typography } from '@/shared/components'\n\nimport s from './TrackCard.module.css'\n\ntype Props = {\n  id: string\n  image: string\n  title: string\n  artists: string\n} & Omit<ReactionButtonsProps, 'className'>\n\nexport const TrackCard = ({\n  id,\n  image,\n  title,\n  artists,\n  reaction,\n  onLike,\n  onDislike,\n  likesCount,\n}: Props) => {\n  return (\n    <Card as={Link} to={`/tracks/${id}`} className={s.card}>\n      <div className={s.image}>\n        <img src={image} alt={title} />\n      </div>\n\n      <Typography variant=\"h3\" className={s.title}>\n        {title}\n      </Typography>\n\n      <Typography variant=\"body3\" className={s.artists}>\n        {artists}\n      </Typography>\n      <ReactionButtons\n        reaction={reaction}\n        onLike={onLike}\n        onDislike={onDislike}\n        likesCount={likesCount}\n      />\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackCard/index.ts",
    "content": "export * from './TrackCard'\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackInfoCell/TrackInfoCell.module.css",
    "content": ".box {\n  display: flex;\n  gap: 21px;\n}\n\n.image {\n  flex-shrink: 0;\n  width: 52px;\n  height: 52px;\n}\n\n.image img {\n  object-fit: cover;\n}\n\n.info {\n  width: 228px;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.title.playing {\n  color: var(--color-accent);\n}\n\n.artists {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackInfoCell/TrackInfoCell.tsx",
    "content": "import clsx from 'clsx'\nimport { Link } from 'react-router'\n\nimport { TableCell, Typography } from '@/shared/components'\n\nimport s from './TrackInfoCell.module.css'\n\nexport const TrackInfoCell = ({\n  image,\n  title,\n  artists,\n  isPlaying,\n  id,\n}: {\n  image: string\n  title: string\n  artists: string[]\n  isPlaying: boolean\n  id: string\n}) => {\n  return (\n    <TableCell>\n      <div className={s.box}>\n        <div className={s.image}>\n          <img src={image} alt={title} />\n        </div>\n        <div className={s.info}>\n          <Typography\n            variant=\"body1\"\n            as={Link}\n            className={clsx(s.title, isPlaying && s.playing)}\n            to={`/tracks/${id}`}>\n            {title}\n          </Typography>\n          <Typography className={s.artists} variant=\"body2\">\n            {artists.join(', ')}\n          </Typography>\n        </div>\n      </div>\n    </TableCell>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackInfoCell/index.ts",
    "content": "export * from './TrackInfoCell.tsx'\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackOverview/TrackOverview.module.css",
    "content": ".container {\n  display: flex;\n  gap: 24px;\n  background: transparent;\n}\n\n.imageContainer {\n  flex-shrink: 0;\n  width: 297px;\n  height: 297px;\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  min-width: 0;\n}\n\n.title {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  margin-bottom: 8px;\n\n  font-size: clamp(var(--font-size-xxl), 8vw, var(--font-size-xxxl));\n  font-weight: 900;\n  line-height: 1;\n  white-space: pre-wrap;\n}\n\n.description {\n  opacity: 0.7;\n}\n\n.info {\n  margin-top: auto;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackOverview/TrackOverview.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_5_HASHTAGS } from '@/features/tags'\n\nimport { TrackOverview } from './TrackOverview'\n\nconst meta: Meta<typeof TrackOverview> = {\n  title: 'entities/TrackOverview',\n  component: TrackOverview,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TrackOverview>\n\nexport const Default: Story = {\n  args: {\n    title: 'Chill Mix',\n    image: 'https://unsplash.it/297/297',\n    releaseDate: '2025-01-01',\n    artists: ['Julia Wolf', 'ayokay', 'Khalid'],\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n\nexport const LongTitle: Story = {\n  args: {\n    title: 'This is a Very Long Track Title That Should Scale Responsively',\n    image: 'https://unsplash.it/299/299',\n    releaseDate: '2025-01-01',\n    artists: ['Julia Wolf', 'ayokay', 'Khalid'],\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackOverview/TrackOverview.tsx",
    "content": "import clsx from 'clsx'\nimport { type ComponentProps } from 'react'\n\nimport { TagsList } from '@/features/tags'\nimport { Typography } from '@/shared/components'\n\nimport s from './TrackOverview.module.css'\n\ntype TrackOverviewProps = {\n  title: string\n  image: string\n  releaseDate: string\n  artists: string[]\n  tags: string[]\n} & ComponentProps<'div'>\n\nexport const TrackOverview = ({\n  title,\n  image,\n  releaseDate,\n  tags,\n  className,\n  artists,\n  ...props\n}: TrackOverviewProps) => {\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <div className={s.imageContainer}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <div className={s.content}>\n        <TagsList tags={tags} entity=\"tracks\" />\n\n        <Typography variant=\"h1\" as=\"h1\" className={s.title}>\n          {title}\n        </Typography>\n\n        <div className={s.info}>\n          <Typography variant=\"body1\">{artists.join(', ')}</Typography>\n          <Typography variant=\"body2\">{releaseDate}</Typography>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackOverview/index.ts",
    "content": "export * from './TrackOverview'\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackRow/TrackRow.module.css",
    "content": ".playing {\n  color: var(--color-accent);\n}\n\n.progress {\n  width: 183px;\n}\n\n.actions {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TrackRow/TrackRow.tsx",
    "content": "import clsx from 'clsx'\nimport type { ReactNode } from 'react'\n\nimport { Progress, TableCell, TableRow, Typography } from '@/shared/components'\nimport { LiveWaveIcon } from '@/shared/icons'\n\nimport { TrackInfoCell } from '../TrackInfoCell'\nimport type { TrackRowData } from '../TracksTable/TracksTable.tsx'\nimport s from './TrackRow.module.css'\n\nexport const TrackRow = <T extends TrackRowData>({\n  trackRow,\n  playingTrackId,\n  playingTrackProgress,\n  renderActionsCell,\n}: {\n  renderActionsCell: (trackRow: T) => ReactNode\n  trackRow: T\n  playingTrackId?: string\n  playingTrackProgress?: number\n}) => {\n  const isPlaying = playingTrackId === trackRow.id\n\n  return (\n    <TableRow>\n      <TableCell className={clsx(isPlaying && s.playing)}>\n        {isPlaying ? <LiveWaveIcon /> : trackRow.index + 1}\n      </TableCell>\n      <TrackInfoCell\n        id={trackRow.id}\n        image={trackRow.image}\n        title={trackRow.title}\n        artists={trackRow.artists}\n        isPlaying={isPlaying}\n      />\n      <TableCell>\n        {isPlaying && (\n          <Progress\n            className={s.progress}\n            value={playingTrackProgress ?? 0}\n            max={trackRow.duration}\n          />\n        )}\n      </TableCell>\n      <TableCell>\n        <Typography variant=\"body2\" as=\"time\" dateTime={trackRow.addedAt}>\n          {new Date(trackRow.addedAt).toLocaleDateString()}\n        </Typography>\n      </TableCell>\n      <TableCell>\n        <div className={s.actions}>{renderActionsCell(trackRow)}</div>\n      </TableCell>\n      <TableCell>\n        <Typography variant=\"body2\">{trackRow.duration}</Typography>\n      </TableCell>\n    </TableRow>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TracksTable/TrackTable.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport {\n  type CurrentUserReaction,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  ReactionButtons,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport { MOCK_TRACKS } from '../../api'\nimport { TracksTable } from './TracksTable'\n\nconst meta: Meta<typeof TracksTable> = {\n  title: 'entities/TracksTable',\n  component: TracksTable,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TracksTable>\n\ntype ReactionsProps =\n  | {\n      likesCount: number\n      dislikesCount: number\n      currentUserReaction: CurrentUserReaction\n    }\n  | {\n      likesCount?: undefined\n      dislikesCount?: undefined\n      currentUserReaction?: undefined\n    }\n\nexport type TrackRowData = {\n  index: number\n  image: string\n  id: string\n  title: string\n  addedAt: string\n  artists: string[]\n  duration: number\n} & ReactionsProps\n\nexport const Default: Story = {\n  args: {\n    trackRows: MOCK_TRACKS.map((track, index) => ({\n      index: index,\n      id: track.id,\n      title: track.attributes.title,\n      image: track.attributes.images.main[0].url,\n      addedAt: track.attributes.addedAt,\n      artists: track.attributes.artists?.map((artist) => artist.name) || [],\n      isPlaying: false,\n      likesCount: track.attributes.likesCount,\n      dislikesCount: track.attributes.dislikesCount,\n      currentUserReaction: track.attributes.currentUserReaction,\n      duration: track.attributes.duration,\n    })),\n    renderTrackRow: (trackRow) => (\n      <TrackRow\n        trackRow={trackRow}\n        playingTrackId={MOCK_TRACKS[0].id}\n        playingTrackProgress={20}\n        renderActionsCell={() => (\n          <>\n            <ReactionButtons\n              reaction={trackRow.currentUserReaction}\n              onLike={() => {}}\n              onDislike={() => {}}\n              likesCount={trackRow.likesCount}\n            />\n\n            <DropdownMenu>\n              <DropdownMenuTrigger>\n                <MoreIcon />\n              </DropdownMenuTrigger>\n\n              <DropdownMenuContent>\n                <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                  Add to playlist\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                  Show text song\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </>\n        )}\n      />\n    ),\n  },\n}\n\nexport const WithoutReactions: Story = {\n  args: {\n    trackRows: MOCK_TRACKS.map((track, index) => ({\n      index: index,\n      id: track.id,\n      title: track.attributes.title,\n      image: track.attributes.images.main[0].url,\n      addedAt: track.attributes.addedAt,\n      artists: track.attributes.artists?.map((artist) => artist.name) || [],\n      duration: track.attributes.duration,\n    })),\n    renderTrackRow: (trackRow) => (\n      <TrackRow\n        trackRow={trackRow}\n        playingTrackId={MOCK_TRACKS[0].id}\n        playingTrackProgress={20}\n        renderActionsCell={() => (\n          <div>\n            <DropdownMenu>\n              <DropdownMenuTrigger>\n                <MoreIcon />\n              </DropdownMenuTrigger>\n\n              <DropdownMenuContent>\n                <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                  Add to playlist\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                  Show text song\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        )}\n      />\n    ),\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TracksTable/TracksTable.tsx",
    "content": "import type { ReactNode } from 'react'\n\nimport {\n  type CurrentUserReaction,\n  Table,\n  TableBody,\n  TableHead,\n  TableHeaderCell,\n  TableRow,\n} from '@/shared/components'\nimport { ClockIcon } from '@/shared/icons'\n\ntype TableColumn = {\n  title: ReactNode\n  width?: string\n}\n\nconst TABLE_COLUMNS: TableColumn[] = [\n  {\n    title: '#',\n    width: '40px',\n  },\n  {\n    title: 'Track',\n  },\n  {\n    title: '',\n  },\n  {\n    title: 'Date added',\n    width: '120px',\n  },\n  {\n    title: 'Actions',\n    width: '150px',\n  },\n  {\n    title: <ClockIcon />,\n    width: '60px',\n  },\n]\n\nexport type TracksTableProps<T extends TrackRowData> = {\n  trackRows: T[]\n  renderTrackRow: (trackRow: T) => ReactNode\n}\n\ntype ReactionsProps =\n  | {\n      likesCount: number\n      dislikesCount: number\n      currentUserReaction: CurrentUserReaction\n    }\n  | {\n      likesCount?: undefined\n      dislikesCount?: undefined\n      currentUserReaction?: undefined\n    }\n\nexport type TrackRowData = {\n  index: number\n  image: string\n  id: string\n  title: string\n  addedAt: string\n  artists: string[]\n  duration: number\n} & ReactionsProps\n\nexport const TracksTable = <T extends TrackRowData>({\n  trackRows,\n  renderTrackRow,\n}: TracksTableProps<T>) => {\n  return (\n    <Table>\n      <TableHead>\n        <TableRow>\n          {TABLE_COLUMNS.map((column, index) => (\n            <TableHeaderCell key={index} style={{ width: column.width }}>\n              {column.title}\n            </TableHeaderCell>\n          ))}\n        </TableRow>\n      </TableHead>\n      <TableBody>{trackRows.map((trackRow) => renderTrackRow(trackRow))}</TableBody>\n    </Table>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/TracksTable/index.ts",
    "content": "export * from './TracksTable'\n"
  },
  {
    "path": "apps/reatom/src/features/tracks/ui/index.ts",
    "content": "export * from './TrackCard'\nexport * from './TrackOverview'\nexport * from './TracksTable'\n"
  },
  {
    "path": "apps/reatom/src/layout/Header/Header.module.css",
    "content": ".header {\n  display: flex;\n  grid-area: header;\n  align-items: center;\n  justify-content: space-between;\n\n  height: var(--header-height);\n  padding: 0 32px;\n}\n"
  },
  {
    "path": "apps/reatom/src/layout/Header/Header.tsx",
    "content": "import { LoginButtonAndModal, ProfileDropdownMenu } from '@/features/auth'\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\n\nimport s from './Header.module.css'\n\nexport const Header = () => {\n  const { data } = useMeQuery()\n\n  return (\n    <header className={s.header}>\n      <div className={s.logo}>Musicfun</div>\n      {data ? <ProfileDropdownMenu avatar={'//unsplash.it/100/100'} /> : <LoginButtonAndModal />}\n    </header>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/layout/Header/index.ts",
    "content": "export { Header } from './Header'\n"
  },
  {
    "path": "apps/reatom/src/layout/Layout.module.css",
    "content": ".grid {\n  display: grid;\n  grid-template: 'header header' var(--header-height) 'sidebar main' 1fr / 310px 1fr;\n  height: 100vh;\n}\n\n.grid.playerOpen {\n  grid-template:\n    'header header' var(--header-height)\n    'sidebar main' 1fr 'player player' var(--player-height) / 310px 1fr;\n}\n\n.main {\n  overflow-y: auto;\n  grid-area: main;\n}\n"
  },
  {
    "path": "apps/reatom/src/layout/Layout.tsx",
    "content": "import clsx from 'clsx'\nimport { Outlet } from 'react-router'\n\nimport { Player } from '@/widgets/Player'\n\nimport { Header } from './Header'\nimport s from './Layout.module.css'\nimport { Sidebar } from './Sidebar'\n\nexport const Layout = () => {\n  const IS_PLAYER_OPEN = true\n\n  return (\n    <div className={clsx(s.grid, IS_PLAYER_OPEN && s.playerOpen)}>\n      <Header />\n      <Sidebar />\n      <main className={s.main}>\n        <Outlet />\n      </main>\n      {IS_PLAYER_OPEN && <Player />}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/layout/Sidebar/MenuLinks/MenuLinks.module.css",
    "content": ".column {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list + .list {\n  padding-top: 20px;\n  border-top: 1px solid var(--color-bg-secondary);\n}\n\n.link {\n  all: unset;\n\n  cursor: pointer;\n\n  display: flex;\n  gap: 16px;\n  align-items: center;\n\n  width: fit-content;\n\n  font-size: var(--font-size-m);\n  font-weight: 700;\n  color: var(--color-text-secondary);\n\n  transition: color 0.2s ease;\n}\n\n.link:hover {\n  color: var(--color-text-primary);\n}\n\n.active {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/reatom/src/layout/Sidebar/MenuLinks/MenuLinks.tsx",
    "content": "import clsx from 'clsx'\nimport { NavLink } from 'react-router'\n\nimport { HomeIcon, LibraryIcon, PlaylistIcon, TrackIcon, UploadIcon } from '@/shared/icons'\nimport { CreateIcon } from '@/shared/icons/CreateIcon'\n\nimport s from './MenuLinks.module.css'\n\ntype MenuLink = {\n  to: string\n  icon: React.ReactNode\n  label: string\n}\n\ntype MenuButton = {\n  onClick: () => void\n  icon: React.ReactNode\n  label: string\n}\n\nconst mainLinks: MenuLink[] = [\n  {\n    to: '/',\n    icon: <HomeIcon width={32} height={32} />,\n    label: 'Home',\n  },\n  {\n    to: '/user/1',\n    icon: <LibraryIcon />,\n    label: 'Your Library',\n  },\n]\n\nconst createLinks: MenuLink[] = [\n  {\n    to: '/tracks',\n    icon: <TrackIcon />,\n    label: 'All Tracks',\n  },\n  {\n    to: '/playlists',\n    icon: <PlaylistIcon />,\n    label: 'All Playlists',\n  },\n]\n\nexport const MenuLinks = () => {\n  const actionButtons: MenuButton[] = [\n    {\n      onClick: () => {},\n      icon: <UploadIcon />,\n      label: 'Upload Track',\n    },\n    {\n      onClick: () => {},\n      icon: <CreateIcon />,\n      label: 'Create Playlist',\n    },\n  ]\n\n  return (\n    <nav className={s.column} aria-label=\"Main navigation\">\n      <ul className={s.list}>\n        {mainLinks.map((props) => (\n          <li key={props.to}>\n            <SidebarLink {...props} />\n          </li>\n        ))}\n      </ul>\n      <ul className={s.list}>\n        {actionButtons.map((props) => (\n          <li key={props.label}>\n            <SidebarButton {...props} />\n          </li>\n        ))}\n      </ul>\n      <ul className={s.list}>\n        {createLinks.map((props) => (\n          <li key={props.to}>\n            <SidebarLink {...props} />\n          </li>\n        ))}\n      </ul>\n    </nav>\n  )\n}\n\nconst SidebarLink = ({ to, icon, label }: MenuLink) => (\n  <NavLink to={to} className={({ isActive }) => clsx(s.link, isActive && s.active)}>\n    {icon}\n    {label}\n  </NavLink>\n)\n\nconst SidebarButton = ({ onClick, icon, label }: MenuButton) => (\n  <button onClick={onClick} className={s.link} type=\"button\">\n    {icon}\n    {label}\n  </button>\n)\n"
  },
  {
    "path": "apps/reatom/src/layout/Sidebar/MenuLinks/index.ts",
    "content": "export * from './MenuLinks'\n"
  },
  {
    "path": "apps/reatom/src/layout/Sidebar/Sidebar.module.css",
    "content": ".sidebar {\n  overflow-y: auto;\n  display: flex;\n  grid-area: sidebar;\n  flex-direction: column;\n\n  height: calc(100vh - var(--header-height) - var(--player-height));\n  padding: 0 30px;\n}\n"
  },
  {
    "path": "apps/reatom/src/layout/Sidebar/Sidebar.tsx",
    "content": "import { MenuLinks } from './MenuLinks'\nimport s from './Sidebar.module.css'\n\nexport const Sidebar = () => {\n  return (\n    <div className={s.sidebar}>\n      <MenuLinks />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/layout/Sidebar/index.ts",
    "content": "export { Sidebar } from './Sidebar'\n"
  },
  {
    "path": "apps/reatom/src/layout/index.ts",
    "content": "export { Layout } from './Layout'\n"
  },
  {
    "path": "apps/reatom/src/pages/MainPage/MainPage.module.css",
    "content": ".mainPage {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n}\n\n.artistsList {\n  --list-gap: 24px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/MainPage/MainPage.tsx",
    "content": "import { PlaylistCard } from '@/entities/playlist'\nimport { MOCK_PLAYLISTS } from '@/features/playlists'\nimport { MOCK_HASHTAGS, TagsList } from '@/features/tags'\nimport { MOCK_TRACKS, TrackCard } from '@/features/tracks'\n\nimport { ContentList, PageWrapper } from '../common'\nimport s from './MainPage.module.css'\n\nexport const MainPage = () => {\n  return (\n    <PageWrapper className={s.mainPage}>\n      <TagsList tags={MOCK_HASHTAGS} />\n      <ContentList\n        title=\"New playlists\"\n        data={MOCK_PLAYLISTS}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            images={playlist.data.attributes.images}\n            description={playlist.data.attributes.description}\n          />\n        )}\n      />\n      <ContentList\n        title=\"New tracks\"\n        data={MOCK_TRACKS}\n        renderItem={(track) => (\n          <TrackCard\n            artists={track.attributes.artist}\n            title={track.attributes.title}\n            id={track.id}\n            image={track.attributes.images.main[0].url}\n            reaction={track.attributes.currentUserReaction}\n            onDislike={() => {}}\n            onLike={() => {}}\n            likesCount={track.attributes.likesCount}\n          />\n        )}\n      />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/MainPage/index.ts",
    "content": "export * from './MainPage'\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistPage/PlaylistPage.module.css",
    "content": ".playlistPage {\n  --page-gradient-color: #adbf22;\n}\n\n.playlistOverview {\n  margin-bottom: 46px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistPage/PlaylistPage.tsx",
    "content": "import { MOCK_PLAYLIST, PlaylistOverview } from '@/features/playlists'\nimport { MOCK_TRACKS, TracksTable } from '@/features/tracks'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { ReactionButtons } from '@/shared/components'\n\nimport { PageWrapper } from '../common'\nimport s from './PlaylistPage.module.css'\nimport { ControlPanel } from './ui/ControlPanel'\n\nexport const PlaylistPage = () => {\n  const playlist = MOCK_PLAYLIST\n\n  return (\n    <PageWrapper className={s.playlistPage}>\n      <PlaylistOverview\n        className={s.playlistOverview}\n        title={playlist.data.attributes.title}\n        image={playlist.data.attributes.images.main[0].url}\n        description={playlist.data.attributes.description.text}\n        tags={playlist.data.attributes.tags}\n      />\n      <ControlPanel />\n      <TracksTable\n        trackRows={MOCK_TRACKS.map((track, index) => ({\n          index,\n          id: track.id,\n          title: track.attributes.title,\n          image: track.attributes.images.main[0].url,\n          addedAt: track.attributes.addedAt,\n          artists: track.attributes.artists?.map((artist) => artist.name) || [],\n          duration: track.attributes.duration,\n          likesCount: track.attributes.likesCount,\n          dislikesCount: track.attributes.dislikesCount,\n          currentUserReaction: track.attributes.currentUserReaction,\n        }))}\n        renderTrackRow={(trackRow) => (\n          <TrackRow\n            trackRow={trackRow}\n            playingTrackId={MOCK_TRACKS[0].id}\n            playingTrackProgress={20}\n            renderActionsCell={(row) => (\n              <ReactionButtons\n                reaction={row.currentUserReaction}\n                onLike={() => {}}\n                onDislike={() => {}}\n                likesCount={row.likesCount}\n              />\n            )}\n          />\n        )}\n      />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistPage/index.ts",
    "content": "export * from './PlaylistPage'\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistPage/ui/ControlPanel/ControlPanel.module.css",
    "content": ".box {\n  display: flex;\n  gap: 24px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.playButton {\n  width: 80px;\n  height: 80px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistPage/ui/ControlPanel/ControlPanel.tsx",
    "content": "import {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  IconButton,\n  ReactionButtons,\n} from '@/shared/components'\nimport { EditIcon, MoreIcon, PlayIcon } from '@/shared/icons'\n\nimport s from './ControlPanel.module.css'\n\nexport const ControlPanel = () => {\n  return (\n    <div className={s.box}>\n      <IconButton className={s.playButton}>\n        <PlayIcon />\n      </IconButton>\n\n      <ReactionButtons reaction={0} onLike={() => {}} onDislike={() => {}} size=\"large\" />\n\n      <DropdownMenu>\n        <DropdownMenuTrigger>\n          <MoreIcon />\n        </DropdownMenuTrigger>\n\n        <DropdownMenuContent align=\"start\">\n          <DropdownMenuItem onClick={() => {}}>\n            <EditIcon />\n            <span>Edit</span>\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistPage/ui/ControlPanel/index.ts",
    "content": "export * from './ControlPanel'\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistsPage/PlaylistsPage.module.css",
    "content": ".title {\n  margin-bottom: 24px;\n}\n\n.pagination {\n  margin-top: 32px;\n}\n\n.controls {\n  margin-bottom: 32px;\n}\n\n.controlsRow {\n  display: flex;\n  gap: 32px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 32px;\n}\n\n.autocomplete {\n  max-width: 513px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistsPage/PlaylistsPage.tsx",
    "content": "import { reatomComponent } from '@reatom/react'\nimport { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'\n\nimport { PlaylistItem } from '@/entities/playlist'\nimport { playlistsListAtom } from '@/features/playlists/model/model.tsx'\nimport { useTags } from '@/features/tags'\nimport {\n  PathsPlaylistsGetParametersQuerySortBy,\n  PathsPlaylistsGetParametersQuerySortDirection,\n  type SchemaGetPlaylistsRequestPayload,\n} from '@/shared/api/schema.ts'\nimport { Autocomplete, Pagination, Typography } from '@/shared/components'\nimport { useDebounceValue } from '@/shared/hooks'\nimport { VU } from '@/shared/utils'\n\nimport { ContentList, PageWrapper, SearchTextField, SortSelect } from '../common'\nimport s from './PlaylistsPage.module.css'\nimport type { ISortConfig, SortOption } from './PlaylistsPage.types.ts'\n\nconst PAGE_SIZE = 5\nconst DEFAULT_PAGE = 1\n\nconst sortConfig: Record<SortOption, ISortConfig> = {\n  newest: {\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.addedAt,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n  },\n  oldest: {\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.addedAt,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.asc,\n  },\n  mostLiked: {\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.likesCount,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n  },\n  leastLiked: {\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.likesCount,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.asc,\n  },\n} as const\n\nexport const PlaylistsPage = reatomComponent(() => {\n  const [pageNumber, setPageNumber] = useState<number>(DEFAULT_PAGE)\n  const [search, setSearch] = useState<string>('')\n  const [sort, setSort] = useState<SortOption>('newest')\n  const [hashtags, setHashtags] = useState<string[]>([])\n\n  const [debouncedSearch] = useDebounceValue(search)\n\n  const { sortBy, sortDirection } = sortConfig[sort]\n\n  useEffect(() => {\n    playlistsListAtom.load(pageNumber)\n  }, [pageNumber])\n\n  const queryParams = useMemo(\n    () => ({\n      search: debouncedSearch,\n      pageNumber,\n      pageSize: PAGE_SIZE,\n      sortBy,\n      sortDirection,\n      tagsIds: hashtags,\n    }),\n    [debouncedSearch, pageNumber, sortBy, sortDirection, hashtags]\n  )\n\n  //const { data, isPending, isError } = usePlaylists(queryParams)\n  const data = playlistsListAtom()\n  const isLoading = playlistsListAtom.isLoading()\n\n  const { data: tagsData, isPending: isTagsLoading } = useTags('')\n\n  const handleSortChange = useCallback((event: ChangeEvent<HTMLSelectElement>) => {\n    const value = event.target.value as SortOption\n\n    setSort(value)\n    setPageNumber(DEFAULT_PAGE)\n  }, [])\n  const handleSearchChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {\n    setSearch(event.target.value)\n    setPageNumber(DEFAULT_PAGE)\n  }, [])\n  const handlePageChange = useCallback((page: SchemaGetPlaylistsRequestPayload['pageNumber']) => {\n    setPageNumber(page)\n  }, [])\n  const handleHashtagsChange = useCallback((tags: SchemaGetPlaylistsRequestPayload['tagsIds']) => {\n    setHashtags(tags || [])\n    setPageNumber(DEFAULT_PAGE)\n  }, [])\n\n  const tagsOptions = useMemo(\n    () =>\n      tagsData?.data?.map((tag) => ({\n        label: tag.name,\n        value: tag.id,\n      })) || [],\n    [tagsData?.data]\n  )\n  const content = useMemo(() => {\n    if (!VU.isValid(data?.data)) {\n      return null\n    }\n\n    if (isLoading) {\n      return <>Loading...</>\n    }\n\n    if (!isLoading && !data) {\n      return <>Playlist loading error. Please try again later.</>\n    }\n\n    if (!VU.isValidArray(data?.data)) {\n      return <>No results found for your search.</>\n    }\n\n    return (\n      <ContentList\n        data={data.data}\n        renderItem={(playlist) => <PlaylistItem playlist={playlist} />}\n      />\n    )\n  }, [data, isLoading])\n\n  return (\n    <PageWrapper>\n      <Typography variant=\"h2\" as=\"h1\" className={s.title}>\n        All Playlists\n      </Typography>\n      <div className={s.controls}>\n        <div className={s.controlsRow}>\n          <SearchTextField\n            placeholder=\"Search playlists\"\n            onChange={handleSearchChange}\n            value={search}\n          />\n          <SortSelect onChange={handleSortChange} value={sort} />\n        </div>\n        <Autocomplete\n          options={tagsOptions}\n          value={hashtags}\n          onChange={handleHashtagsChange}\n          label=\"Hashtags\"\n          placeholder={isTagsLoading ? 'Loading tags...' : 'Search by hashtags'}\n          disabled={isTagsLoading}\n          className={s.autocomplete}\n        />\n      </div>\n      {content}\n      <Pagination\n        className={s.pagination}\n        page={pageNumber}\n        pagesCount={data?.meta.pagesCount || 1}\n        onPageChange={handlePageChange}\n      />\n    </PageWrapper>\n  )\n})\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistsPage/PlaylistsPage.types.ts",
    "content": "import type { SchemaGetPlaylistsRequestPayload } from '@/shared/api/schema.ts'\n\nexport type SortOption = 'mostLiked' | 'leastLiked' | 'newest' | 'oldest'\n\nexport interface ISortConfig {\n  sortBy: SchemaGetPlaylistsRequestPayload['sortBy']\n  sortDirection: SchemaGetPlaylistsRequestPayload['sortDirection']\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/PlaylistsPage/index.ts",
    "content": "export * from './PlaylistsPage'\n"
  },
  {
    "path": "apps/reatom/src/pages/TrackPage/TrackPage.module.css",
    "content": ".trackPage {\n  --page-gradient-color: #9a3426;\n}\n\n.trackOverview {\n  margin-bottom: 46px;\n}\n\n.title {\n  margin-bottom: 18px;\n}\n\n.search {\n  margin-bottom: 24px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/TrackPage/TrackPage.tsx",
    "content": "import { PlaylistCard } from '@/entities/playlist'\nimport { MOCK_PLAYLISTS } from '@/features/playlists'\nimport { TrackOverview } from '@/features/tracks'\nimport { Pagination, SearchField, Typography } from '@/shared/components'\n\nimport { ContentList, PageWrapper } from '../common'\nimport s from './TrackPage.module.css'\nimport { ControlPanel } from './ui/ControlPanel'\n\nexport const TrackPage = () => {\n  return (\n    <PageWrapper className={s.trackPage}>\n      <TrackOverview\n        className={s.trackOverview}\n        title=\"Chill Mix\"\n        image=\"https://unsplash.it/297/297\"\n        releaseDate=\"2025-01-01\"\n        artists={['Julia Wolf', 'ayokay', 'Khalid']}\n        tags={['chill', 'mood', 'relax']}\n      />\n\n      <ControlPanel />\n\n      <Typography variant=\"h2\" className={s.title}>\n        In which playlist is the track?\n      </Typography>\n\n      <SearchField placeholder=\"Search playlists\" className={s.search} />\n\n      <ContentList\n        data={[...MOCK_PLAYLISTS]}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            images={playlist.data.attributes.images}\n            description={playlist.data.attributes.description}\n          />\n        )}\n      />\n      <Pagination className={s.pagination} page={1} pagesCount={2} onPageChange={() => {}} />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/TrackPage/index.ts",
    "content": "export * from './TrackPage'\n"
  },
  {
    "path": "apps/reatom/src/pages/TrackPage/ui/ControlPanel/ControlPanel.module.css",
    "content": ".box {\n  display: flex;\n  gap: 24px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.playButton {\n  width: 80px;\n  height: 80px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/TrackPage/ui/ControlPanel/ControlPanel.tsx",
    "content": "import {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  IconButton,\n  ReactionButtons,\n} from '@/shared/components'\nimport { AddToPlaylistIcon, EditIcon, MoreIcon, PlayIcon, TextIcon } from '@/shared/icons'\n\nimport s from './ControlPanel.module.css'\n\nexport const ControlPanel = () => {\n  return (\n    <div className={s.box}>\n      <IconButton className={s.playButton}>\n        <PlayIcon />\n      </IconButton>\n\n      <ReactionButtons\n        reaction={0}\n        onLike={() => {}}\n        onDislike={() => {}}\n        size=\"large\"\n        likesCount={438}\n      />\n\n      <DropdownMenu>\n        <DropdownMenuTrigger>\n          <MoreIcon />\n        </DropdownMenuTrigger>\n\n        <DropdownMenuContent align=\"start\">\n          <DropdownMenuItem onClick={() => {}}>\n            <EditIcon />\n            <span>Edit</span>\n          </DropdownMenuItem>\n\n          <DropdownMenuItem onClick={() => {}}>\n            <AddToPlaylistIcon />\n            <span>Add to playlist</span>\n          </DropdownMenuItem>\n\n          <DropdownMenuItem onClick={() => {}}>\n            <TextIcon />\n            <span>Show lyrics</span>\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/TrackPage/ui/ControlPanel/index.ts",
    "content": "export * from './ControlPanel'\n"
  },
  {
    "path": "apps/reatom/src/pages/TracksPage/TracksPage.module.css",
    "content": ".title {\n  margin-bottom: 24px;\n}\n\n.controls {\n  margin-bottom: 32px;\n}\n\n.controlsRow {\n  display: flex;\n  gap: 32px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 32px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/TracksPage/TracksPage.tsx",
    "content": "import { useState } from 'react'\n\nimport { MOCK_ARTISTS } from '@/features/artists/api/artists-api'\nimport { MOCK_HASHTAGS } from '@/features/tags'\nimport { MOCK_TRACKS, TracksTable } from '@/features/tracks'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport {\n  Autocomplete,\n  DropdownMenu,\n  DropdownMenuTrigger,\n  ReactionButtons,\n  Typography,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport { PageWrapper, SearchTextField, SortSelect } from '../common'\nimport s from './TracksPage.module.css'\n\nexport const TracksPage = () => {\n  const [hashtags, setHashtags] = useState<string[]>([])\n  const [artists, setArtists] = useState<string[]>([])\n\n  return (\n    <PageWrapper>\n      <Typography variant=\"h2\" as=\"h1\" className={s.title}>\n        All Tracks\n      </Typography>\n      <div className={s.controls}>\n        <div className={s.controlsRow}>\n          <SearchTextField placeholder=\"Search tracks\" onChange={() => {}} />\n          <SortSelect onChange={() => {}} />\n        </div>\n        <div className={s.controlsRow}>\n          <Autocomplete\n            options={MOCK_HASHTAGS.map((hashtag) => ({\n              label: hashtag,\n              value: hashtag,\n            }))}\n            value={hashtags}\n            onChange={setHashtags}\n            label=\"Hashtags\"\n            placeholder=\"Search by hashtags\"\n            className={s.autocomplete}\n          />\n          <Autocomplete\n            options={MOCK_ARTISTS.map((artist) => ({\n              label: artist.name,\n              value: artist.id,\n            }))}\n            value={artists}\n            onChange={setArtists}\n            label=\"Artists\"\n            placeholder=\"Search by artists\"\n            className={s.autocomplete}\n          />\n        </div>\n      </div>\n\n      <TracksTable\n        trackRows={MOCK_TRACKS.map((track, index) => ({\n          index,\n          id: track.id,\n          title: track.attributes.title,\n          image: track.attributes.images.main[0].url,\n          addedAt: track.attributes.addedAt,\n          artists: track.attributes.artists?.map((artist) => artist.name) || [],\n          duration: track.attributes.duration,\n          likesCount: track.attributes.likesCount,\n          dislikesCount: track.attributes.dislikesCount,\n          currentUserReaction: track.attributes.currentUserReaction,\n        }))}\n        renderTrackRow={(trackRow) => (\n          <TrackRow\n            key={trackRow.id}\n            trackRow={trackRow}\n            playingTrackId={MOCK_TRACKS[0].id}\n            playingTrackProgress={20}\n            renderActionsCell={() => (\n              <>\n                <ReactionButtons\n                  reaction={trackRow.currentUserReaction}\n                  onLike={() => {}}\n                  onDislike={() => {}}\n                  likesCount={trackRow.likesCount}\n                />\n                <DropdownMenu>\n                  <DropdownMenuTrigger>\n                    <MoreIcon />\n                  </DropdownMenuTrigger>\n                </DropdownMenu>\n              </>\n            )}\n          />\n        )}\n      />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/TracksPage/index.ts",
    "content": "export * from './TracksPage'\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/UserPage.module.css",
    "content": ".userPage {\n  --page-gradient-color: #b8a661;\n\n  display: flex;\n  flex-direction: column;\n  gap: 24px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/UserPage.tsx",
    "content": "import { PageWrapper } from '../common'\nimport { UserInfo, UserTabs } from './ui'\nimport s from './UserPage.module.css'\n\nexport const UserPage = () => {\n  return (\n    <PageWrapper className={s.userPage}>\n      <UserInfo />\n      <UserTabs />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/index.ts",
    "content": "export * from './UserPage'\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserInfo/UserInfo.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  align-items: center;\n}\n\n.avatar {\n  overflow: hidden;\n  width: 192px;\n  height: 192px;\n  border-radius: 50%;\n}\n\n.descriptionList {\n  display: flex;\n  gap: 23px;\n  margin: 0;\n}\n\n.descriptionItem {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.descriptionItem dd {\n  margin: 0;\n}\n\n.descriptionItem dt {\n  font-size: var(--font-size-s);\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserInfo/UserInfo.tsx",
    "content": "import { Button, Typography } from '@/shared/components'\nimport { EditIcon } from '@/shared/icons'\n\nimport s from './UserInfo.module.css'\n\nexport const UserInfo = () => {\n  return (\n    <div className={s.box}>\n      <div className={s.avatar}>\n        <img src={'https://unsplash.it/192/192'} alt=\"User avatar\" />\n      </div>\n      <Typography variant=\"h2\">Martin Fowler</Typography>\n\n      <Button variant=\"secondary\">\n        <EditIcon /> Edit profile\n      </Button>\n      <dl className={s.descriptionList}>\n        <div className={s.descriptionItem}>\n          <Typography as=\"dd\" variant=\"body1\">\n            58\n          </Typography>\n          <Typography as=\"dt\" variant=\"body2\">\n            Playlists\n          </Typography>\n        </div>\n        <div className={s.descriptionItem}>\n          <Typography as=\"dd\" variant=\"body1\">\n            100\n          </Typography>\n          <Typography as=\"dt\" variant=\"body2\">\n            Tracks\n          </Typography>\n        </div>\n      </dl>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserInfo/index.ts",
    "content": "export * from './UserInfo'\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/LikedTracksTab/LikedTracksTab.module.css",
    "content": ""
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/LikedTracksTab/LikedTracksTab.tsx",
    "content": "import { MOCK_TRACKS } from '@/features/tracks'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { TracksTable } from '@/features/tracks/ui/TracksTable/TracksTable'\nimport { ReactionButtons } from '@/shared/components'\nimport { DropdownMenu, DropdownMenuTrigger } from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nexport const LikedTracksTab = () => {\n  return (\n    <TracksTable\n      trackRows={MOCK_TRACKS.map((track, index) => ({\n        index,\n        id: track.id,\n        title: track.attributes.title,\n        image: track.attributes.images.main[0].url,\n        addedAt: track.attributes.addedAt,\n        artists: track.attributes.artists?.map((artist) => artist.name) || [],\n        duration: track.attributes.duration,\n        likesCount: track.attributes.likesCount,\n        dislikesCount: track.attributes.dislikesCount,\n        currentUserReaction: track.attributes.currentUserReaction,\n      }))}\n      renderTrackRow={(trackRow) => (\n        <TrackRow\n          trackRow={trackRow}\n          playingTrackId={MOCK_TRACKS[0].id}\n          playingTrackProgress={20}\n          renderActionsCell={() => (\n            <>\n              <ReactionButtons\n                reaction={trackRow.currentUserReaction}\n                onLike={() => {}}\n                onDislike={() => {}}\n                likesCount={trackRow.likesCount}\n              />\n              <DropdownMenu>\n                <DropdownMenuTrigger>\n                  <MoreIcon />\n                </DropdownMenuTrigger>\n              </DropdownMenu>\n            </>\n          )}\n        />\n      )}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/LikedTracksTab/index.ts",
    "content": "export * from './LikedTracksTab'\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/MyLikedPlaylistsTab.tsx",
    "content": "import { PlaylistCard } from '@/entities/playlist'\nimport { MOCK_PLAYLISTS } from '@/features/playlists'\nimport { ContentList } from '@/pages/common'\nimport { Pagination } from '@/shared/components'\n\nexport const MyLikedPlaylistsTab = () => {\n  return (\n    <>\n      <ContentList\n        data={[...MOCK_PLAYLISTS]}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            images={playlist.data.attributes.images}\n            description={playlist.data.attributes.description}\n          />\n        )}\n      />\n      <Pagination page={1} pagesCount={2} onPageChange={() => {}} />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/index.ts",
    "content": "export * from './MyLikedPlaylistsTab'\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/PlaylistsTab/PlaylistsTab.module.css",
    "content": ".createPlaylistButton {\n  display: block;\n\n  width: 328px;\n  height: 54px;\n  margin: 0 auto;\n  margin-bottom: 24px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/PlaylistsTab/PlaylistsTab.tsx",
    "content": "import { useState } from 'react'\n\nimport { PlaylistCard } from '@/entities/playlist'\nimport { CreatePlaylistModal, MOCK_PLAYLISTS } from '@/features/playlists'\nimport { ContentList } from '@/pages/common'\nimport { Button, Pagination } from '@/shared/components'\n\nimport s from './PlaylistsTab.module.css'\n\nexport const PlaylistsTab = () => {\n  const [isCreatePlaylistModalOpen, setIsCreatePlaylistModalOpen] = useState(false) // STATE FOR TESTING\n\n  const openCreatePlaylistModal = () => {\n    setIsCreatePlaylistModalOpen(true)\n  }\n\n  return (\n    <>\n      <Button className={s.createPlaylistButton} onClick={openCreatePlaylistModal}>\n        Create Playlist\n      </Button>\n\n      {isCreatePlaylistModalOpen && (\n        <CreatePlaylistModal onClose={() => setIsCreatePlaylistModalOpen(false)} />\n      )}\n      <ContentList\n        data={[...MOCK_PLAYLISTS]}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            images={playlist.data.attributes.images || []}\n            description={playlist.data.attributes.description}\n          />\n        )}\n      />\n      <Pagination page={1} pagesCount={2} onPageChange={() => {}} />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/PlaylistsTab/index.ts",
    "content": "export * from './PlaylistsTab'\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/TracksTab/TracksTab.module.css",
    "content": ".uploadTrackButton {\n  display: block;\n\n  width: 328px;\n  height: 54px;\n  margin: 0 auto;\n  margin-bottom: 24px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/TracksTab/TracksTab.tsx",
    "content": "// import { useState } from 'react'\n\nimport { MOCK_TRACKS, TracksTable } from '@/features/tracks'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { Button } from '@/shared/components'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport s from './TracksTab.module.css'\n\nexport const TracksTab = () => {\n  // const [isUploadTrackModalOpen, setIsUploadTrackModalOpen] = useState(false) // STATE FOR TESTING\n\n  const openUploadTrackModal = () => {\n    // setIsUploadTrackModalOpen(true)\n  }\n\n  return (\n    <>\n      <Button className={s.uploadTrackButton} onClick={openUploadTrackModal}>\n        Upload Track\n      </Button>\n      <TracksTable\n        trackRows={MOCK_TRACKS.map((track, index) => ({\n          index,\n          id: track.id,\n          title: track.attributes.title,\n          image: track.attributes.images.main[0].url,\n          addedAt: track.attributes.addedAt,\n          artists: track.attributes.artists?.map((artist) => artist.name) || [],\n          duration: track.attributes.duration,\n        }))}\n        renderTrackRow={(trackRow) => (\n          <TrackRow\n            trackRow={trackRow}\n            renderActionsCell={() => (\n              <DropdownMenu>\n                <DropdownMenuTrigger>\n                  <MoreIcon />\n                </DropdownMenuTrigger>\n                <DropdownMenuContent>\n                  <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                  <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                    Add to playlist\n                  </DropdownMenuItem>\n                  <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                    Show text song\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n            )}\n          />\n        )}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/TracksTab/index.ts",
    "content": ""
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/UserTabs.tsx",
    "content": "import { Tabs, TabsContent, TabsList, TabsTrigger, Typography } from '@/shared/components'\n\nimport { LikedTracksTab } from './LikedTracksTab'\nimport { MyLikedPlaylistsTab } from './MyLikedPlaylistsTab'\nimport { PlaylistsTab } from './PlaylistsTab'\nimport { TracksTab } from './TracksTab/TracksTab'\n\nexport const UserTabs = () => {\n  const isProfileOwner = true // STATE FOR TESTING\n\n  return (\n    <Tabs defaultValue=\"playlists\">\n      <TabsList>\n        <TabsTrigger value=\"playlists\">Playlists</TabsTrigger>\n        <TabsTrigger value=\"tracks\">Tracks</TabsTrigger>\n        {isProfileOwner && (\n          <>\n            <TabsTrigger value=\"liked-playlists\">Liked Playlists</TabsTrigger>\n            <TabsTrigger value=\"liked-tracks\">Liked Tracks</TabsTrigger>\n          </>\n        )}\n      </TabsList>\n      <TabsContent value=\"playlists\">\n        <PlaylistsTab />\n      </TabsContent>\n      <TabsContent value=\"tracks\">\n        <TracksTab />\n      </TabsContent>\n      {isProfileOwner && (\n        <>\n          <TabsContent value=\"liked-playlists\">\n            <MyLikedPlaylistsTab />\n          </TabsContent>\n          <TabsContent value=\"liked-tracks\">\n            <LikedTracksTab />\n          </TabsContent>\n        </>\n      )}\n    </Tabs>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/UserTabs/index.ts",
    "content": "export * from './UserTabs'\n"
  },
  {
    "path": "apps/reatom/src/pages/UserPage/ui/index.ts",
    "content": "export * from './UserInfo'\nexport * from './UserTabs'\n"
  },
  {
    "path": "apps/reatom/src/pages/auth/OAuthRedirect/OAuthCallback.module.css",
    "content": ".title {\n  text-align: center;\n  font-size: 250px;\n  margin: 0;\n}\n\n.subtitle {\n  text-align: center;\n  font-size: 50px;\n  margin: 0;\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/auth/OAuthRedirect/OAuthCallback.tsx",
    "content": "import { useEffect } from 'react'\n\nexport const OAuthCallback = () => {\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const code = url.searchParams.get('code') // или code/state, если flow другой\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, '*') // Лучше заменить \"*\" на точный origin\n    }\n\n    window.close()\n  }, [])\n\n  return <p>Welcome...</p>\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/common/ContentList/ContentList.module.css",
    "content": ".title {\n  margin-bottom: 20px;\n}\n\n.list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--list-gap, 8px);\n  padding-bottom: 8px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/common/ContentList/ContentList.tsx",
    "content": "import clsx from 'clsx'\n\nimport { Typography } from '@/shared/components/Typography/Typography'\n\nimport s from './ContentList.module.css'\n\ntype ContentListProps<T> = {\n  title?: string\n  data: T[]\n  renderItem: (item: T) => React.ReactNode\n  listClassName?: string\n}\n\nexport const ContentList = <T,>({\n  title,\n  data,\n  renderItem,\n  listClassName,\n}: ContentListProps<T>) => {\n  return (\n    <section>\n      {title && (\n        <Typography variant=\"h2\" className={s.title}>\n          {title}\n        </Typography>\n      )}\n      <ul className={clsx(s.list, listClassName)}>\n        {data.map((item, index) => (\n          <li key={index}>{renderItem(item)}</li>\n        ))}\n      </ul>\n    </section>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/common/ContentList/index.ts",
    "content": "export * from './ContentList'\n"
  },
  {
    "path": "apps/reatom/src/pages/common/PageWrapper/PageWrapper.module.css",
    "content": ".wrapper {\n  min-height: calc(100vh - var(--header-height));\n  padding: 30px 40px;\n  background: linear-gradient(\n    180deg,\n    var(--page-gradient-color, #3333a3) 0,\n    var(--color-bg-secondary) 300px,\n    var(--color-bg-secondary) 100%\n  );\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/common/PageWrapper/PageWrapper.tsx",
    "content": "import clsx from 'clsx'\n\nimport s from './PageWrapper.module.css'\n\ntype PageWrapperProps = {\n  children: React.ReactNode\n  className?: string\n}\n\nexport const PageWrapper = ({ children, className }: PageWrapperProps) => {\n  return <div className={clsx(s.wrapper, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/common/PageWrapper/index.ts",
    "content": "export * from './PageWrapper'\n"
  },
  {
    "path": "apps/reatom/src/pages/common/SearchTextField/SearchTextField.tsx",
    "content": "import { TextField, type TextFieldProps } from '@/shared/components'\nimport { SearchIcon } from '@/shared/icons'\n\nexport const SearchTextField = (props: Omit<TextFieldProps, 'icon' | 'inputSize'>) => {\n  return (\n    <TextField\n      {...props}\n      icon={<SearchIcon width={20} height={20} />}\n      inputSize=\"l\"\n      autoComplete=\"off\"\n    />\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/common/SearchTextField/index.ts",
    "content": "export * from './SearchTextField'\n"
  },
  {
    "path": "apps/reatom/src/pages/common/SortSelect/SortSelect.module.css",
    "content": ".selectLabel {\n  display: flex;\n  flex-shrink: 0;\n  gap: 8px;\n  align-items: center;\n\n  width: 210px;\n}\n\n.select {\n  flex-shrink: 0;\n  width: 145px;\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/common/SortSelect/SortSelect.tsx",
    "content": "import { Select, type SelectProps } from '@/shared/components'\n\nimport s from './SortSelect.module.css'\n\nexport const SortSelect = (props: Omit<SelectProps, 'options'>) => {\n  return (\n    <label className={s.selectLabel}>\n      Sort By\n      <Select\n        {...props}\n        options={[\n          { value: 'newest', label: 'Newest first' },\n          { value: 'oldest', label: 'Oldest first' },\n          { value: 'mostLiked', label: 'Most liked' },\n          { value: 'leastLiked', label: 'Least liked' },\n        ]}\n        className={s.select}\n      />\n    </label>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/pages/common/SortSelect/index.ts",
    "content": "export * from './SortSelect'\n"
  },
  {
    "path": "apps/reatom/src/pages/common/index.ts",
    "content": "export * from './ContentList'\nexport * from './PageWrapper'\nexport * from './SearchTextField'\nexport * from './SortSelect'\n"
  },
  {
    "path": "apps/reatom/src/pages/index.ts",
    "content": "export * from './MainPage'\nexport * from './PlaylistPage'\nexport * from './PlaylistsPage'\nexport * from './TrackPage'\nexport * from './TracksPage'\nexport * from './UserPage'\n"
  },
  {
    "path": "apps/reatom/src/shared/api/client.ts",
    "content": "import createClient, { type Middleware } from 'openapi-fetch'\n\nimport type { paths } from './schema.ts'\n\nconst config = {\n  baseURL: null as string | null,\n  apiKey: null as string | null,\n  getAccessToken: null as (() => Promise<string | null>) | null,\n  saveAccessToken: null as ((accessToken: string | null) => Promise<void>) | null,\n  getRefreshToken: null as (() => Promise<string | null>) | null,\n  saveRefreshToken: null as ((refreshToken: string | null) => Promise<void>) | null,\n  toManyRequestsErrorHandler: null as ((message: string | null) => void) | null,\n  logoutHandler: null as (() => void) | null,\n}\n\nexport const setClientConfig = (newConfig: Partial<typeof config>) => {\n  Object.assign(config, newConfig)\n  _client = undefined // пере-инициализируем\n}\n\nexport const getClientConfig = () => ({ ...config })\n\n/* ------------------------------------------------------------------ */\n/* 2.  Mutex для refresh-а                                             */\n/* ------------------------------------------------------------------ */\nlet refreshPromise: Promise<string> | null = null\n\nexport function makeRefreshToken(): Promise<string> {\n  if (!refreshPromise) {\n    // 1) создаём «замок» сразу\n    refreshPromise = (async (): Promise<string> => {\n      const refreshToken = await config.getRefreshToken!()\n      if (!refreshToken) throw new Error('No refresh token')\n\n      const res = await fetch(`${config.baseURL}/auth/refresh`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'API-KEY': config.apiKey!,\n        },\n        body: JSON.stringify({ refreshToken }),\n      })\n      if (res.status !== 201) throw new Error('Refresh failed')\n\n      const { accessToken, refreshToken: newRT } = await res.json()\n      await config.saveAccessToken!(accessToken)\n      await config.saveRefreshToken!(newRT)\n\n      return accessToken\n    })().finally(() => {\n      refreshPromise = null // 2) снимаем «замок»\n    })\n  }\n\n  return refreshPromise\n}\n\nconst authMiddleware: Middleware = {\n  /* ---------- REQUEST -------------------------------------------------- */\n  async onRequest({ request }) {\n    request.headers.set('API-KEY', config.apiKey!)\n\n    const token = await config.getAccessToken?.()\n    if (token) request.headers.set('Authorization', `Bearer ${token}`)\n    ;(request as any)._retryClone = request.clone()\n\n    return request\n  },\n  async onResponse({ request, response }) {\n    const req = request as Request & { _retry: boolean }\n\n    if (response.status === 429) {\n      const { message } = await response.clone().json()\n      config.toManyRequestsErrorHandler?.(message)\n    }\n\n    if (response.status !== 401 || request.url.includes('/auth/refresh')) {\n      return response // всё ок\n    }\n\n    // уже пытались? -> отдаём 401 наружу, чтобы не зациклиться\n    if (req._retry) return response\n    req._retry = true\n\n    try {\n      const newToken = await makeRefreshToken()\n\n      // повторяем исходный запрос с новым токеном\n      const orig = (req as any)._retryClone as Request // клон с целым body\n      const retry = new Request(orig, { headers: new Headers(orig.headers) })\n      retry.headers.set('Authorization', `Bearer ${newToken}`)\n      return await fetch(retry)\n    } catch (error) {\n      console.log(error)\n      // refresh не удался → чистим хранилище, отдаём 401\n      await config.saveAccessToken!(null)\n      await config.saveRefreshToken!(null)\n      await config.logoutHandler?.()\n      return response\n    }\n  },\n}\n\nlet _client: ReturnType<typeof createClient<paths>> | undefined\n\nconst LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0'])\n\nfunction isLocalClient(): boolean {\n  if (typeof window === 'undefined') return false // не клиент\n  const h = window.location.hostname\n  return LOCAL_HOSTNAMES.has(h) || h.endsWith('.localhost')\n}\n\nexport function assertApiConfig() {\n  if (!config.baseURL) {\n    const msg = 'baseURL is required. Call setClientConfig({ baseURL })'\n    console.error(msg)\n    throw new Error(msg)\n  }\n  if (isLocalClient() && !config.apiKey) {\n    const msg =\n      'apiKey is required when running client on localhost. Call setClientConfig({ apiKey })'\n    console.error(msg)\n    throw new Error(msg)\n  }\n}\n\nexport const getClient = () => {\n  if (_client) return _client\n\n  assertApiConfig()\n\n  const client = createClient<paths>({ baseUrl: config.baseURL! })\n  client.use(authMiddleware)\n  _client = client\n  return _client\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/api/schema.ts",
    "content": "/**\n * This file was auto-generated by openapi-typescript.\n * Do not make direct changes to the file.\n */\n\nexport interface paths {\n  '/playlists/my': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Get my playlists\n     * @deprecated\n     */\n    get: operations['PlaylistsController_getMyPlaylists']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Retrieve all playlists\n     * @description Query parameters must conform to the **GetPlaylistsRequestPayload** schema.\n     */\n    get: operations['PlaylistsPublicController_getPlaylists']\n    put?: never\n    /** Create a new playlist */\n    post: operations['PlaylistsController_createPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get a single playlist by ID */\n    get: operations['PlaylistsPublicController_getPlaylistById']\n    /** Update a playlist */\n    put: operations['PlaylistsController_updatePlaylist']\n    post?: never\n    /** Delete a playlist */\n    delete: operations['PlaylistsController_deletePlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Reorder playlists */\n    put: operations['PlaylistsController_reorderPlaylist']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/images/main': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /**\n     * Upload playlist cover\n     * @description Minimum height — 500px; image must be square\n     */\n    post: operations['PlaylistsController_uploadMainImage']\n    /** Delete playlist cover */\n    delete: operations['PlaylistsController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get list of all tracks in all playlists */\n    get: operations['TracksPublicController_getAllTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get list of tracks in a playlist */\n    get: operations['TracksPublicController_getPlaylistTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get track details by ID */\n    get: operations['TracksPublicController_getTrackDetails']\n    /** Update track information */\n    put: operations['TracksController_updateTrack']\n    post?: never\n    /** Permanently delete a track */\n    delete: operations['TracksController_deleteTrackCompletely']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Like or toggle like on a track */\n    post: operations['TracksPublicController_likeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Dislike or toggle dislike on a track */\n    post: operations['TracksPublicController_dislikeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove user reaction from a track */\n    delete: operations['TracksPublicController_removeTrackReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Like a playlist */\n    post: operations['PlaylistsPublicController_likePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Dislike a playlist */\n    post: operations['PlaylistsPublicController_dislikePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove user reaction from a playlist */\n    delete: operations['PlaylistsPublicController_removePlaylistReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks/{trackId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Reorder tracks in a playlist */\n    put: operations['TracksController_reorderTrack']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Add a track to your playlist */\n    post: operations['TracksController_addTrackToPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove a track from your playlist */\n    delete: operations['TracksController_unbindTrackFromPlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/actions/publish': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Publish a track (make it publicly available) */\n    post: operations['TracksController_publishTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/cover': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Upload track cover */\n    post: operations['TracksController_uploadTrackCover']\n    /** Delete track cover */\n    delete: operations['TracksController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/upload': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a track with MP3 file upload */\n    post: operations['TracksController_uploadTrackMp3']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a new artist */\n    post: operations['ArtistsController_createArtist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Search artists by substring */\n    get: operations['ArtistsController_searchArtist']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Delete an artist by ID */\n    delete: operations['ArtistsController_deleteArtist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/oauth-redirect': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * OAuth redirect\n     * @description The callback URL to redirect after granting access, <a target=\"_blank\" href=\"https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\">https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid</a>\n     */\n    get: operations['AuthController_OauthRedirect']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/login': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Log in using the code received after OAuth authorization redirect */\n    post: operations['AuthController_login']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/refresh': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Refresh refresh/access token pair */\n    post: operations['AuthController_refresh']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/logout': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Deactivate refresh token */\n    post: operations['AuthController_logout']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/me': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get current user by access token */\n    get: operations['AuthController_getMe']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a new tag */\n    post: operations['TagsController_createTag']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Search tags by substring */\n    get: operations['TagsController_searchTags']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Delete a tag by ID */\n    delete: operations['TagsController_deleteTag']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n}\nexport type webhooks = Record<string, never>\nexport interface components {\n  schemas: {\n    UserOutputDTO: {\n      /** @description Unique identifier of the user */\n      id: string\n      /** @description Name of the user */\n      name: string\n    }\n    /**\n     * @description Type of the image size (e.g., original, thumbnail variants)\n     * @enum {string}\n     */\n    ImageSizeType: ImageSizeType\n    ImageDto: {\n      /** @description Type of the image size (e.g., original, thumbnail variants) */\n      type: components['schemas']['ImageSizeType']\n      /** @description Image width in pixels */\n      width: number\n      /** @description Image height in pixels */\n      height: number\n      /** @description Image file size in bytes */\n      fileSize: number\n      /** @description Full public URL of the image */\n      url: string\n    }\n    PlaylistImagesOutputDTO: {\n      /** @description Original images and thumbnail previews */\n      main?: components['schemas']['ImageDto'][]\n    }\n    GetTagOutput: {\n      /** @description Unique identifier of the tag */\n      id: string\n      /** @description Original name of the tag */\n      name: string\n    }\n    /**\n     * @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike\n     * @enum {number}\n     */\n    ReactionValue: ReactionValue\n    PlaylistAttributesDto: {\n      /** @description Title of the playlist */\n      title: string\n      /** @description Description of the playlist */\n      description: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Order index of the playlist */\n      order: number\n      /** @description User who created the playlist */\n      user: components['schemas']['UserOutputDTO']\n      /** @description Images associated with the playlist */\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      /** @description Tags linked to the playlist */\n      tags: components['schemas']['GetTagOutput'][]\n      /** @description Total number of likes for this playlist */\n      likesCount: number\n      /** @description Total number of dislikes for this playlist */\n      dislikesCount: number\n      /** @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistListItemJsonApiData: {\n      /** @description Unique identifier of the playlist */\n      id: string\n      /**\n       * @description Resource type (should be \"playlists\")\n       * @example playlists\n       */\n      type: string\n      /** @description Attributes of the playlist resource */\n      attributes: components['schemas']['PlaylistAttributesDto']\n    }\n    GetMyPlaylistsOutput: {\n      /** @description Array of playlist resource objects owned by the current user */\n      data: components['schemas']['PlaylistListItemJsonApiData'][]\n    }\n    CreatePlaylistRequestPayload: {\n      /** @description Playlist title (1 to 100 characters) */\n      title: string\n      /** @description Playlist description (up to 1000 characters) */\n      description: string | null\n    }\n    PlaylistOutputAttributes: {\n      /** @description Title of the playlist */\n      title: string\n      /** @description Description of the playlist */\n      description: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Order index of the playlist */\n      order: number\n      /** @description User who created the playlist */\n      user: components['schemas']['UserOutputDTO']\n      /** @description Images associated with the playlist */\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      /** @description Tags linked to the playlist */\n      tags: components['schemas']['GetTagOutput'][]\n      /** @description Total number of likes for this playlist */\n      likesCount: number\n      /** @description Total number of dislikes for this playlist */\n      dislikesCount: number\n      /** @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistOutput: {\n      /** @description Unique identifier of the playlist */\n      id: string\n      /**\n       * @description Resource type (should be \"playlists\")\n       * @example playlists\n       */\n      type: string\n      /** @description Playlist attributes object */\n      attributes: components['schemas']['PlaylistOutputAttributes']\n    }\n    GetPlaylistOutput: {\n      /** @description JSON:API single-resource response wrapper */\n      data: components['schemas']['PlaylistOutput']\n    }\n    UpdatePlaylistRequestPayload: {\n      /** @description Playlist title (1 – 100 characters) */\n      title: string\n      /**\n       * @description Playlist description (up to 1000 characters)\n       * @example Cool playlist\n       */\n      description: string | null\n      /** @description Tag IDs to associate with the playlist (0 – 5 items; [] = clear tags) */\n      tagIds: string[]\n    }\n    ReorderPlaylistsRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID of the playlist after which the current playlist should be inserted. Send null to place the playlist at the beginning of the list.\n       */\n      putAfterItemId: string | null\n    }\n    GetImagesOutput: {\n      /** @description List of original images and thumbnail versions (e.g., original, 320x180, etc.) */\n      main?: components['schemas']['ImageDto'][]\n    }\n    GetTracksRequestPayload: {\n      /**\n       * @description Page number for pagination (starting from 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Page size for pagination (between 1 and 20)\n       * @default 20\n       */\n      pageSize: number\n      /** @description Search term for filtering playlists by name */\n      search?: string\n      /**\n       * @description Field by which to sort tracks\n       * @default publishedAt\n       * @enum {string}\n       */\n      sortBy: PathsPlaylistsTracksGetParametersQuerySortBy\n      /**\n       * @description Sort direction (ascending or descending)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection\n      /** @description Filter by tag IDs (multiple values allowed) */\n      tagsIds?: string[]\n      /** @description Filter by artist IDs (multiple values allowed) */\n      artistsIds?: string[]\n      /** @description Filter by user ID (track creator's ID) */\n      userId?: string\n      /** @description If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n      includeDrafts?: boolean\n      /**\n       * @description Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination.\n       * @default offset\n       * @enum {string}\n       */\n      paginationType: PathsPlaylistsTracksGetParametersQueryPaginationType\n      /** @description Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\". */\n      cursor?: string | null\n    }\n    JsonApiErrorSource: {\n      /**\n       * @description e.g. \"/data/attributes/field\"\n       * @example /data/attributes/field\n       */\n      pointer?: string\n      /**\n       * @description e.g. \"?queryParam\"\n       * @example ?queryParam\n       */\n      parameter?: string\n    }\n    JsonApiError: {\n      /**\n       * @description HTTP status code as a string\n       * @example 404\n       */\n      status: string\n      /**\n       * @description Application-specific error code\n       * @example E123\n       */\n      code?: Record<string, never>\n      /**\n       * @description Short, human-readable summary\n       * @example Not Found\n       */\n      title?: string\n      /**\n       * @description Detailed explanation\n       * @example User with ID 123 not found\n       */\n      detail?: string\n      /** @description Pointer to the associated entity in the request */\n      source?: components['schemas']['JsonApiErrorSource']\n      /** @description Any extra data */\n      meta?: Record<string, never>\n    }\n    JsonApiErrorDocument: {\n      /** @description Array of one or more errors */\n      errors: components['schemas']['JsonApiError'][]\n      /** @description e.g. timestamp, path, traceId, etc. */\n      meta?: Record<string, never>\n    }\n    AttachmentDto: {\n      /** @description Unique identifier of the entity */\n      id: string\n      /**\n       * Format: date-time\n       * @description Date and time when the entity was added\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the entity was last updated\n       */\n      updatedAt: string\n      /** @description Version number of the entity (for concurrency control) */\n      version: number\n      /**\n       * @description Public URL to access the uploaded file\n       * @example https://cdn.example.com/uploads/track123/cover.jpg\n       */\n      url: string\n      /**\n       * @description MIME type of the file\n       * @example image/jpeg\n       */\n      contentType: string\n      /**\n       * @description Original filename uploaded by the user\n       * @example cover.jpg\n       */\n      originalName: string\n      /**\n       * @description Size of the file in bytes\n       * @example 34872\n       */\n      fileSize: number\n    }\n    TrackListItemOutputAttributes: {\n      title: string\n      addedAt: string\n      likesCount: number\n      attachments: components['schemas']['AttachmentDto'][]\n      images: components['schemas']['GetImagesOutput']\n      user: components['schemas']['UserOutputDTO']\n      /**\n       * @description 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк\n       * @enum {number}\n       */\n      currentUserReaction: ReactionValue\n      isPublished: boolean\n      publishedAt?: string\n    }\n    ArtistRelationship: {\n      id: string\n      type: string\n    }\n    ArtistsRelationship: {\n      data: components['schemas']['ArtistRelationship'][]\n    }\n    TrackRelationships: {\n      artists: components['schemas']['ArtistsRelationship']\n    }\n    TrackListItemOutput: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackListItemOutputAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMetaWithPagingAndCursor: {\n      page: number\n      pageSize: number\n      /** @description Total count may be absent when using keyset pagination */\n      totalCount: number | null\n      /** @description Total number of pages */\n      pagesCount: number | null\n      /** @description Cursor for the next page */\n      nextCursor: string | null\n    }\n    OmitTypeClass: {\n      /** @description Name of the artist */\n      name: string\n    }\n    IncludedArtistOutput: {\n      id: string\n      type: string\n      attributes: components['schemas']['OmitTypeClass']\n    }\n    GetTrackListOutput: {\n      data: components['schemas']['TrackListItemOutput'][]\n      meta: components['schemas']['JsonApiMetaWithPagingAndCursor']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    PlaylistTrackAttributes: {\n      /** @description Title of the track */\n      title: string\n      /** @description Order index of the track in the playlist */\n      order: number\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added to the playlist (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated in the playlist (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Attachments related to the track */\n      attachments: components['schemas']['AttachmentDto'][]\n      /** @description Images associated with the track */\n      images: components['schemas']['GetImagesOutput']\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – liked; -1 – disliked\n       * @enum {number|null}\n       */\n      currentUserReaction: ReactionValue\n    }\n    GetPlaylistTrackListOutputData: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['PlaylistTrackAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMeta: {\n      totalCount: number\n    }\n    GetPlaylistTrackListOutput: {\n      data: components['schemas']['GetPlaylistTrackListOutputData'][]\n      meta: components['schemas']['JsonApiMeta']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    GetArtistOutput: {\n      /** @description Unique identifier of the artist */\n      id: string\n      /** @description Name of the artist */\n      name: string\n    }\n    TrackDetailsAttributes: {\n      /** @description Track title */\n      title: string\n      /** @description Track lyrics text */\n      lyrics?: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate?: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Duration of the track in seconds */\n      duration: number\n      /** @description Total number of likes for this track */\n      likesCount: number\n      /**\n       * @deprecated\n       * @description Total number of dislikes for this track\n       */\n      dislikesCount: number\n      /** @description List of attachments related to the track */\n      attachments: components['schemas']['AttachmentDto'][]\n      /** @description Images associated with the track */\n      images: components['schemas']['GetImagesOutput']\n      /** @description Tags associated with the track */\n      tags: components['schemas']['GetTagOutput'][]\n      /** @description Artists associated with the track */\n      artists: components['schemas']['GetArtistOutput'][]\n      user: components['schemas']['UserOutputDTO']\n      /** @description Publication status of the track */\n      isPublished: boolean\n      /**\n       * Format: date-time\n       * @description Publication date in ISO 8601 format\n       */\n      publishedAt?: string | null\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n       * @enum {number}\n       */\n      currentUserReaction: ReactionValue\n    }\n    TrackDetailsData: {\n      /** @description Unique identifier of the track */\n      id: string\n      /**\n       * @description Resource type (should be \"tracks\")\n       * @example tracks\n       */\n      type: string\n      /** @description Detailed attributes of the track resource */\n      attributes: components['schemas']['TrackDetailsAttributes']\n    }\n    GetTrackDetailsOutput: {\n      /** @description JSON:API single-track details response wrapper */\n      data: components['schemas']['TrackDetailsData']\n    }\n    ReactionOutput: {\n      objectId: string\n      /** @enum {number} */\n      value: ReactionValue\n      likes: number\n      dislikes: number\n    }\n    GetPlaylistsRequestPayload: {\n      /**\n       * @description Page number for pagination (starting from 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Page size for pagination (between 1 and 20)\n       * @default 20\n       */\n      pageSize: number\n      /** @description Search term for filtering playlists by name */\n      search?: string\n      /**\n       * @description Field by which to sort playlists\n       * @default addedAt\n       * @enum {string}\n       */\n      sortBy: PathsPlaylistsGetParametersQuerySortBy\n      /**\n       * @description Sort direction (ascending or descending)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection\n      /** @description Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n      tagsIds?: string[]\n      /** @description Filter by user ID (playlist creator’s ID) */\n      userId?: string\n      /** @description Filter by track ID – only playlists containing this track will be returned */\n      trackId?: string\n    }\n    JsonApiMetaWithPaging: {\n      totalCount: number\n      page: number\n      pageSize: number\n      pagesCount: number\n    }\n    GetPlaylistsOutput: {\n      /** @description Array of playlist resource objects */\n      data: components['schemas']['PlaylistListItemJsonApiData'][]\n      /** @description Pagination metadata for the playlists list */\n      meta: components['schemas']['JsonApiMetaWithPaging']\n    }\n    ReorderTracksRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID of the track after which the current track should be inserted. Send null to place the track at the beginning of the list.\n       * @example a1b2c3d4-e5f6-7890-abcd-1234567890ef\n       */\n      putAfterItemId: string | null\n    }\n    UpdateTrackRequestPayload: {\n      /** @description Track title (1 to 100 characters) */\n      title: string\n      /** @description Track lyrics (up to 5000 characters) */\n      lyrics: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate: string | null\n      /** @description Array of tag IDs to associate with the track (up to 5) */\n      tagIds: string[]\n      /** @description Array of artist IDs to associate with the track (up to 5) */\n      artistsIds: string[]\n    }\n    TrackOutputAttributes: {\n      /** @description Track title */\n      title: string\n      /** @description Track lyrics text */\n      lyrics?: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate?: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Duration of the track in seconds */\n      duration: number\n      /** @description Total number of likes for this track */\n      likesCount: number\n      /**\n       * @deprecated\n       * @description Total number of dislikes for this track\n       */\n      dislikesCount: number\n      /** @description List of attachments related to the track */\n      attachments: components['schemas']['AttachmentDto'][]\n      /** @description Images associated with the track */\n      images: components['schemas']['GetImagesOutput']\n      /** @description Tags associated with the track */\n      tags: components['schemas']['GetTagOutput'][]\n      /** @description Artists associated with the track */\n      artists: components['schemas']['GetArtistOutput'][]\n      user: components['schemas']['UserOutputDTO']\n      /** @description Publication status of the track */\n      isPublished: boolean\n      /**\n       * Format: date-time\n       * @description Publication date in ISO 8601 format\n       */\n      publishedAt?: string | null\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n       * @enum {number}\n       */\n      currentUserReaction: ReactionValue\n    }\n    TrackOutput: {\n      /** @description Unique identifier of the track */\n      id: string\n      /**\n       * @description Resource type (should be \"tracks\")\n       * @example tracks\n       */\n      type: string\n      /** @description Attributes of the track resource */\n      attributes: components['schemas']['TrackOutputAttributes']\n    }\n    GetTrackOutput: {\n      /** @description JSON:API single-track response wrapper */\n      data: components['schemas']['TrackOutput']\n    }\n    AddTrackToPlaylistRequestPayload: {\n      /** @description ID of the track to add to the playlist */\n      trackId: string\n    }\n    CreateArtistRequestPayload: {\n      /** @description Artist name (must be between 2 and 30 characters) */\n      name: string\n    }\n    LoginRequestPayload: {\n      /** @description Authorization code received from OAuth server after redirect */\n      code: string\n      /**\n       * @description Specify the same redirect URI used in the initial OAuth server request\n       * @example http://localhost:3000/oauth2/callback\n       */\n      redirectUri: string\n      /**\n       * @description Access token lifetime (default \"3m\"); must be a string like \"60s\", \"3m\", \"2h\", or \"1d\"\n       * @example 3m\n       */\n      accessTokenTTL?: string\n      /** @description Refresh token lifetime: if true, 30 days; if false, 30 minutes. accessTokenTTL must not exceed the refresh token lifetime */\n      rememberMe: boolean\n    }\n    RefreshOutput: {\n      refreshToken: string\n      accessToken: string\n    }\n    BadRequestException: Record<string, never>\n    UnauthorizedException: Record<string, never>\n    RefreshRequestPayload: {\n      refreshToken: string\n    }\n    LogoutRequestPayload: {\n      refreshToken: string\n    }\n    GetMeOutput: {\n      userId: string\n      login: string\n    }\n    CreateTagRequestPayload: {\n      /** @description Tag name (2 to 30 characters) */\n      name: string\n    }\n    /**\n     * Format: binary\n     * @description Файл в multipart/form-data\n     */\n    BinaryFile: string\n  }\n  responses: never\n  parameters: never\n  requestBodies: never\n  headers: never\n  pathItems: never\n}\nexport type SchemaUserOutputDto = components['schemas']['UserOutputDTO']\nexport type SchemaImageSizeType = components['schemas']['ImageSizeType']\nexport type SchemaImageDto = components['schemas']['ImageDto']\nexport type SchemaPlaylistImagesOutputDto = components['schemas']['PlaylistImagesOutputDTO']\nexport type SchemaGetTagOutput = components['schemas']['GetTagOutput']\nexport type SchemaReactionValue = components['schemas']['ReactionValue']\nexport type SchemaPlaylistAttributesDto = components['schemas']['PlaylistAttributesDto']\nexport type SchemaPlaylistListItemJsonApiData = components['schemas']['PlaylistListItemJsonApiData']\nexport type SchemaGetMyPlaylistsOutput = components['schemas']['GetMyPlaylistsOutput']\nexport type SchemaCreatePlaylistRequestPayload =\n  components['schemas']['CreatePlaylistRequestPayload']\nexport type SchemaPlaylistOutputAttributes = components['schemas']['PlaylistOutputAttributes']\nexport type SchemaPlaylistOutput = components['schemas']['PlaylistOutput']\nexport type SchemaGetPlaylistOutput = components['schemas']['GetPlaylistOutput']\nexport type SchemaUpdatePlaylistRequestPayload =\n  components['schemas']['UpdatePlaylistRequestPayload']\nexport type SchemaReorderPlaylistsRequestPayload =\n  components['schemas']['ReorderPlaylistsRequestPayload']\nexport type SchemaGetImagesOutput = components['schemas']['GetImagesOutput']\nexport type SchemaGetTracksRequestPayload = components['schemas']['GetTracksRequestPayload']\nexport type SchemaJsonApiErrorSource = components['schemas']['JsonApiErrorSource']\nexport type SchemaJsonApiError = components['schemas']['JsonApiError']\nexport type SchemaJsonApiErrorDocument = components['schemas']['JsonApiErrorDocument']\nexport type SchemaAttachmentDto = components['schemas']['AttachmentDto']\nexport type SchemaTrackListItemOutputAttributes =\n  components['schemas']['TrackListItemOutputAttributes']\nexport type SchemaArtistRelationship = components['schemas']['ArtistRelationship']\nexport type SchemaArtistsRelationship = components['schemas']['ArtistsRelationship']\nexport type SchemaTrackRelationships = components['schemas']['TrackRelationships']\nexport type SchemaTrackListItemOutput = components['schemas']['TrackListItemOutput']\nexport type SchemaJsonApiMetaWithPagingAndCursor =\n  components['schemas']['JsonApiMetaWithPagingAndCursor']\nexport type SchemaOmitTypeClass = components['schemas']['OmitTypeClass']\nexport type SchemaIncludedArtistOutput = components['schemas']['IncludedArtistOutput']\nexport type SchemaGetTrackListOutput = components['schemas']['GetTrackListOutput']\nexport type SchemaPlaylistTrackAttributes = components['schemas']['PlaylistTrackAttributes']\nexport type SchemaGetPlaylistTrackListOutputData =\n  components['schemas']['GetPlaylistTrackListOutputData']\nexport type SchemaJsonApiMeta = components['schemas']['JsonApiMeta']\nexport type SchemaGetPlaylistTrackListOutput = components['schemas']['GetPlaylistTrackListOutput']\nexport type SchemaGetArtistOutput = components['schemas']['GetArtistOutput']\nexport type SchemaTrackDetailsAttributes = components['schemas']['TrackDetailsAttributes']\nexport type SchemaTrackDetailsData = components['schemas']['TrackDetailsData']\nexport type SchemaGetTrackDetailsOutput = components['schemas']['GetTrackDetailsOutput']\nexport type SchemaReactionOutput = components['schemas']['ReactionOutput']\nexport type SchemaGetPlaylistsRequestPayload = components['schemas']['GetPlaylistsRequestPayload']\nexport type SchemaJsonApiMetaWithPaging = components['schemas']['JsonApiMetaWithPaging']\nexport type SchemaGetPlaylistsOutput = components['schemas']['GetPlaylistsOutput']\nexport type SchemaReorderTracksRequestPayload = components['schemas']['ReorderTracksRequestPayload']\nexport type SchemaUpdateTrackRequestPayload = components['schemas']['UpdateTrackRequestPayload']\nexport type SchemaTrackOutputAttributes = components['schemas']['TrackOutputAttributes']\nexport type SchemaTrackOutput = components['schemas']['TrackOutput']\nexport type SchemaGetTrackOutput = components['schemas']['GetTrackOutput']\nexport type SchemaAddTrackToPlaylistRequestPayload =\n  components['schemas']['AddTrackToPlaylistRequestPayload']\nexport type SchemaCreateArtistRequestPayload = components['schemas']['CreateArtistRequestPayload']\nexport type SchemaLoginRequestPayload = components['schemas']['LoginRequestPayload']\nexport type SchemaRefreshOutput = components['schemas']['RefreshOutput']\nexport type SchemaBadRequestException = components['schemas']['BadRequestException']\nexport type SchemaUnauthorizedException = components['schemas']['UnauthorizedException']\nexport type SchemaRefreshRequestPayload = components['schemas']['RefreshRequestPayload']\nexport type SchemaLogoutRequestPayload = components['schemas']['LogoutRequestPayload']\nexport type SchemaGetMeOutput = components['schemas']['GetMeOutput']\nexport type SchemaCreateTagRequestPayload = components['schemas']['CreateTagRequestPayload']\nexport type SchemaBinaryFile = components['schemas']['BinaryFile']\nexport type $defs = Record<string, never>\nexport interface operations {\n  PlaylistsController_getMyPlaylists: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of playlists retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMyPlaylistsOutput']\n        }\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylists: {\n    parameters: {\n      query?: {\n        /** @description Page number for pagination (starting from 1) */\n        pageNumber?: number\n        /** @description Page size for pagination (between 1 and 20) */\n        pageSize?: number\n        /** @description Search term for filtering playlists by name */\n        search?: string\n        /** @description Field by which to sort playlists */\n        sortBy?: PathsPlaylistsGetParametersQuerySortBy\n        /** @description Sort direction (ascending or descending) */\n        sortDirection?: PathsPlaylistsGetParametersQuerySortDirection\n        /** @description Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n        tagsIds?: string[]\n        /** @description Filter by user ID (playlist creator’s ID) */\n        userId?: string\n        /** @description Filter by track ID – only playlists containing this track will be returned */\n        trackId?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: JSON:API list of playlists with pagination */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistsOutput']\n        }\n      }\n    }\n  }\n  PlaylistsController_createPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Playlist created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Forbidden: Playlist creation limit exceeded */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylistById: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the playlist */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Playlist retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Not Found: Playlist with the given ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_updatePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Playlist updated successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Bad Request: Validation error (e.g., tag limit exceeded) */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: You do not have permission to update this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deletePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Playlist deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Insufficient permissions to delete this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_reorderPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderPlaylistsRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Playlist order updated successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist or putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_uploadMainImage: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @description Maximum size 1 MB; minimum height 500px; image must be square */\n          file: components['schemas']['BinaryFile']\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Cover uploaded successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetImagesOutput']\n        }\n      }\n      /** @description Bad Request: Invalid image format or dimensions */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No permission to upload cover for this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Cover deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Removing another user’s playlist cover is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getAllTracks: {\n    parameters: {\n      query?: {\n        /** @description Page number for pagination (starting from 1) */\n        pageNumber?: number\n        /** @description Page size for pagination (between 1 and 20) */\n        pageSize?: number\n        /** @description Search term for filtering playlists by name */\n        search?: string\n        /** @description Field by which to sort tracks */\n        sortBy?: PathsPlaylistsTracksGetParametersQuerySortBy\n        /** @description Sort direction (ascending or descending) */\n        sortDirection?: PathsPlaylistsGetParametersQuerySortDirection\n        /** @description Filter by tag IDs (multiple values allowed) */\n        tagsIds?: string[]\n        /** @description Filter by artist IDs (multiple values allowed) */\n        artistsIds?: string[]\n        /** @description Filter by user ID (track creator's ID) */\n        userId?: string\n        /** @description If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n        includeDrafts?: boolean\n        /** @description Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination. */\n        paginationType?: PathsPlaylistsTracksGetParametersQueryPaginationType\n        /** @description Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\". */\n        cursor?: string | null\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Paginated list of tracks */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackListOutput']\n        }\n      }\n      /** @description Bad Request: invalid query parameters */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['JsonApiErrorDocument']\n        }\n      }\n    }\n  }\n  TracksPublicController_getPlaylistTracks: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the playlist to retrieve tracks for */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of tracks in the playlist */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistTrackListOutput']\n        }\n      }\n      /** @description Not Found: Playlist with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getTrackDetails: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the track to retrieve details for */\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Track details with attachments */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description Not Found: Track with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_updateTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdateTrackRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Track updated successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackOutput']\n        }\n      }\n      /** @description Bad Request: Tag or artist limit exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Editing another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track or playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCompletely: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track permanently deleted */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Deleting another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_likeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: User reaction recorded and counters updated */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid track ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_dislikeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: User reaction recorded and counters updated */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid track ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_removeTrackReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Reaction removed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_likePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: Like recorded successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid playlist ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_dislikePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: Dislike recorded successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid playlist ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_removePlaylistReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Reaction removed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_reorderTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderTracksRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Track order updated successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Bad Request: Cannot place a track after itself */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track or putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_addTrackToPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['AddTrackToPlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Track added to the playlist successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist or track limit exceeded (max 10 tracks) */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_unbindTrackFromPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track removed from the playlist */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_publishTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track published successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Publishing another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Track is already published */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the track for which the cover is being uploaded */\n        trackId: string\n      }\n      cookie?: never\n    }\n    /** @description Image file:<br/>\n     *             • Field name — <code>cover</code><br/>\n     *             • Allowed MIME types — <code>image/jpeg</code>, <code>image/png</code>, <code>image/gif</code><br/>\n     *             • Maximum size — <code>100 KB</code> */\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** Format: binary */\n          cover: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Cover uploaded successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetImagesOutput']\n        }\n      }\n      /** @description Bad Request: Invalid file or size exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Cannot upload a cover for another user’s track */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Cover deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Removing another user's track cover is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackMp3: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @example My cool track */\n          title: string\n          /** Format: binary */\n          file: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Track created successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackOutput']\n        }\n      }\n      /** @description Bad Request: Invalid file format or file size exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Internal Server Error: Error saving file or track */\n      500: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_createArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateArtistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Artist created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetArtistOutput']\n        }\n      }\n      /** @description Bad Request: Validation error or invalid input */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Limit of 100 artists per user reached */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Artist with the given name already exists */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_searchArtist: {\n    parameters: {\n      query: {\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of artists matching the search */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetArtistOutput'][]\n        }\n      }\n    }\n  }\n  ArtistsController_deleteArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Artist deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Artist is attached to tracks or was created by another user */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Artist with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_OauthRedirect: {\n    parameters: {\n      query?: {\n        /** @description The callback URL to redirect after grand access,\n         *          https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid */\n        callbackUrl?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Redirect executed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_login: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LoginRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Token pair retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Bad Request: Invalid request format or required parameters are missing */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['BadRequestException']\n        }\n      }\n      /** @description Unauthorized: Code is invalid, expired, missing, or redirectUri does not match */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_refresh: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['RefreshRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Token pair refreshed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Unauthorized: Refresh token is invalid, expired, or missing */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_logout: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LogoutRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Refresh token deactivated; access token remains valid. */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_getMe: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Successfully retrieved user information */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMeOutput']\n        }\n      }\n      /** @description Unauthorized: access token is missing or invalid */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_createTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateTagRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Tag created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput']\n        }\n      }\n      /** @description Bad Request: Validation error */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Limit of 100 tags per user reached */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Tag with the given name already exists */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_searchTags: {\n    parameters: {\n      query: {\n        /** @description Substring to search tags by (using normalized name) */\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of matching tags */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput'][]\n        }\n      }\n      /** @description Bad Request: Invalid search query */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_deleteTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the tag to delete */\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Tag deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Tag was created by another user or is attached to tracks or playlists */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Tag with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n}\nexport enum PathsPlaylistsGetParametersQuerySortBy {\n  addedAt = 'addedAt',\n  likesCount = 'likesCount',\n}\nexport enum PathsPlaylistsGetParametersQuerySortDirection {\n  asc = 'asc',\n  desc = 'desc',\n}\nexport enum PathsPlaylistsTracksGetParametersQuerySortBy {\n  publishedAt = 'publishedAt',\n  likesCount = 'likesCount',\n}\nexport enum PathsPlaylistsTracksGetParametersQueryPaginationType {\n  offset = 'offset',\n  cursor = 'cursor',\n}\nexport enum ImageSizeType {\n  original = 'original',\n  thumbnail = 'thumbnail',\n  medium = 'medium',\n}\nexport enum ReactionValue {\n  Value0 = 0,\n  Value1 = 1,\n  ValueMinus1 = -1,\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/api/types.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\nexport type MainImage = components['schemas']['ImageDto'][]\n"
  },
  {
    "path": "apps/reatom/src/shared/api/utils/json-api-error.ts",
    "content": "export interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport type ExtractError<T> = T extends { error?: infer E } ? E : unknown\n\n/* --- типы ошибок, совпадающие с фильтром -------------------------------- */\nexport interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport function isJsonApiErrorDocument(error: unknown): error is JsonApiErrorDocument {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    // @ts-expect-error type no matter\n    Array.isArray(error.errors)\n  )\n}\n\nexport function parseJsonApiErrors(errorDoc: JsonApiErrorDocument): {\n  fieldErrors: Record<string, string>\n  globalErrors: string[]\n} {\n  const fieldErrors: Record<string, string> = {}\n  const globalErrors: string[] = []\n\n  for (const err of errorDoc.errors) {\n    const msg = err.detail ?? err.title ?? 'Unknown error'\n    const ptr = err.source?.pointer\n    if (ptr) {\n      // убираем префикс JSON:API\n      const field = ptr.replace(/^\\/data\\/attributes\\//, '')\n      fieldErrors[field] = msg\n    } else {\n      globalErrors.push(msg)\n    }\n  }\n\n  return { fieldErrors, globalErrors }\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/api/utils/request-wrapper.ts",
    "content": "// types/api.ts\n\nimport { type ExtractError } from './json-api-error.ts'\n\n//-----------------------------------------------------------------------------\n// utils/requestWrapper.ts\n//-----------------------------------------------------------------------------\n// «Умный» обёртчик: Infers Data и Error из P,\n// возвращает Promise<Data>, а в случае ошибки — throw Error\nexport type ExtractData<T> = T extends { data?: infer D } ? NonNullable<D> : never\n\nexport async function requestWrapper<P extends Promise<{ data?: unknown; error?: unknown }>>(\n  promise: P\n): Promise<ExtractData<Awaited<P>>> {\n  const res = (await promise) as Awaited<P>\n  if ((res as { error?: unknown }).error) {\n    // здесь E = ExtractError<Awaited<P>>\n    throw (res as { error: ExtractError<Awaited<P>> }).error\n  }\n  return (res as { data: ExtractData<Awaited<P>> }).data\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/AudioPlayer/AudioPlayer.module.css",
    "content": ".player {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  width: 100%;\n  min-height: 64px;\n\n  background: var(--color-bg-primary);\n}\n\n.trackInfo {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  min-width: 200px;\n}\n\n.cover {\n  width: 112px;\n  height: 112px;\n  border-radius: 4px;\n  background: var(--color-bg-card);\n}\n\n.cover img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.info {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.playerControls {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 8px;\n  align-items: center;\n}\n\n.controls {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n}\n\n.playPauseButton {\n  width: 48px;\n  height: 48px;\n}\n\n.active {\n  color: var(--color-accent);\n}\n\n.iconButton.active:hover,\n.iconButton.active:focus {\n  color: var(--color-accent);\n}\n\n.progressBar {\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  max-width: 632px;\n}\n\n.time {\n  min-width: 36px;\n  font-size: var(--font-size-xs);\n  color: var(--color-text-secondary);\n  text-align: center;\n}\n\n.progress {\n  cursor: pointer;\n\n  height: 5px;\n  border: none;\n  border-radius: 4px;\n\n  accent-color: var(--color-text-primary);\n}\n\n.trackProgress {\n  width: 100%;\n  max-width: 550px;\n}\n\n.volumeColumn {\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n\n  min-width: 160px;\n  padding-right: 32px;\n}\n\n.volumeProgress {\n  width: 119px;\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/AudioPlayer/AudioPlayer.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { AudioPlayer } from './AudioPlayer.tsx'\n\nconst meta = {\n  title: 'Components/Player',\n  component: AudioPlayer,\n  parameters: {},\n  args: {},\n} satisfies Meta<typeof AudioPlayer>\n\nexport default meta\n\nconst demoTrack = {\n  src: 'https://cdn.uppbeat.io/audio-files/c636d7c86452449b1203fc0bded83e29/4358717fc9da477a52fb18a6cbd3afcc/d154b5ce5ff1a05ae8115a3c678062e8/STREAMING-dreamland-matrika-main-version-31140-02-25.mp3',\n  cover: 'https://unsplash.it/112/112',\n  title: 'Play It Safe',\n  artist: 'Julia Wolf',\n}\n\nexport const Basic = {\n  render: () => {\n    const [isPlaying, setIsPlaying] = useState(false)\n    const [isShuffle, setIsShuffle] = useState(false)\n    const [isRepeat, setIsRepeat] = useState(false)\n\n    const [track] = useState(demoTrack)\n    return (\n      <AudioPlayer\n        {...track}\n        isPlaying={isPlaying}\n        setIsPlaying={setIsPlaying}\n        onNext={() => {}}\n        onPrevious={() => {}}\n        isShuffle={isShuffle}\n        isRepeat={isRepeat}\n        onShuffle={() => setIsShuffle(!isShuffle)}\n        onRepeat={() => setIsRepeat(!isRepeat)}\n      />\n    )\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/AudioPlayer/AudioPlayer.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ComponentProps, useRef, useState } from 'react'\n\nimport {\n  PauseIcon,\n  PlayIcon,\n  RepeatIcon,\n  ShuffleIcon,\n  SkipNextIcon,\n  SkipPreviousIcon,\n  VolumeIcon,\n  VolumeMuteIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './AudioPlayer.module.css'\n\nexport type PlayerProps = {\n  src: string\n  cover: string\n  title: string\n  artist: string\n  isPlaying: boolean\n  setIsPlaying: (isPlaying: boolean) => void\n  onNext: () => void\n  onPrevious: () => void\n  isShuffle: boolean\n  isRepeat: boolean\n  onShuffle: () => void\n  onRepeat: () => void\n} & ComponentProps<'div'>\n\nexport const AudioPlayer = ({\n  src,\n  cover,\n  title,\n  artist,\n  isPlaying,\n  setIsPlaying,\n  onNext,\n  onPrevious,\n  isShuffle,\n  isRepeat,\n  onShuffle,\n  onRepeat,\n  className,\n  ...props\n}: PlayerProps) => {\n  const audioRef = useRef<HTMLAudioElement | null>(null)\n  const [currentTime, setCurrentTime] = useState(0)\n  const [volume, setVolume] = useState(1)\n  const [duration, setDuration] = useState(0)\n\n  const handlePlayPause = () => {\n    const audio = audioRef.current\n    if (!audio) return\n\n    if (isPlaying) {\n      audio.pause()\n    } else {\n      audio.play().catch((e) => {\n        console.error('Audio play error:', e)\n      })\n    }\n\n    setIsPlaying(!isPlaying)\n  }\n\n  const handleChangeTime = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const time = Number(e.target.value)\n    setCurrentTime(time)\n    if (audioRef.current) {\n      audioRef.current.currentTime = time\n    }\n  }\n\n  const handleVolume = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newVolume = Number(e.target.value)\n    setVolume(newVolume)\n    if (audioRef.current) {\n      audioRef.current.volume = newVolume\n    }\n  }\n\n  const handleVolumeMute = () => {\n    const newVolume = volume > 0 ? 0 : 1\n    setVolume(newVolume)\n    if (audioRef.current) {\n      audioRef.current.volume = newVolume\n    }\n  }\n\n  return (\n    <div className={clsx(s.player, className)} {...props}>\n      <audio\n        ref={audioRef}\n        src={src}\n        onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}\n        onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}\n      />\n\n      <div className={s.trackInfo}>\n        <div className={s.cover}>\n          <img src={cover} alt=\"cover\" />\n        </div>\n        <div className={s.info}>\n          <Typography variant=\"body1\" as=\"h3\">\n            {title}\n          </Typography>\n          <Typography variant=\"body2\" as=\"p\">\n            {artist}\n          </Typography>\n        </div>\n      </div>\n\n      <div className={s.playerControls}>\n        <div className={s.controls}>\n          <IconButton onClick={onShuffle} className={clsx(s.iconButton, isShuffle && s.active)}>\n            <ShuffleIcon />\n          </IconButton>\n          <IconButton onClick={onPrevious}>\n            <SkipPreviousIcon />\n          </IconButton>\n          <IconButton className={s.playPauseButton} onClick={handlePlayPause}>\n            {isPlaying ? <PauseIcon /> : <PlayIcon />}\n          </IconButton>\n          <IconButton onClick={onNext}>\n            <SkipNextIcon />\n          </IconButton>\n          <IconButton onClick={onRepeat} className={clsx(s.iconButton, isRepeat && s.active)}>\n            <RepeatIcon />\n          </IconButton>\n        </div>\n\n        <div className={s.progressBar}>\n          <span className={s.time}>{format(currentTime)}</span>\n          <input\n            type=\"range\"\n            min={0}\n            max={duration}\n            value={currentTime}\n            onChange={handleChangeTime}\n            className={clsx(s.progress, s.trackProgress)}\n          />\n          <span className={s.time}>{format(duration)}</span>\n        </div>\n      </div>\n\n      <div className={s.volumeColumn}>\n        <IconButton onClick={handleVolumeMute}>\n          {volume > 0 ? <VolumeIcon /> : <VolumeMuteIcon />}\n        </IconButton>\n        <input\n          type=\"range\"\n          min={0}\n          max={1}\n          step={0.01}\n          value={volume}\n          onChange={handleVolume}\n          className={clsx(s.progress, s.volumeProgress)}\n        />\n      </div>\n    </div>\n  )\n}\n\nconst format = (sec: number) => {\n  const m = Math.floor(sec / 60)\n  const s = Math.floor(sec % 60)\n  return `${m}:${s.toString().padStart(2, '0')}`\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/AudioPlayer/index.ts",
    "content": "export * from './AudioPlayer.tsx'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Autocomplete/Autocomplete.module.css",
    "content": ".container {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.labelError {\n  color: var(--color-text-error);\n}\n\n.inputWrapper {\n  position: relative;\n\n  display: flex;\n  flex-wrap: wrap;\n  gap: 6px;\n  align-items: center;\n\n  min-height: 48px;\n  padding: 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.inputWrapper:hover:not(.disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.inputWrapper.focused {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-primary);\n}\n\n.inputWrapper.error {\n  border-color: var(--color-text-error);\n}\n\n.inputWrapper.disabled {\n  cursor: not-allowed;\n  background-color: var(--color-disabled);\n}\n\n.tag {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n\n  padding: 2px 6px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 16px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.inputContainer {\n  position: relative;\n\n  display: flex;\n  flex: 1;\n  align-items: center;\n\n  min-width: 120px;\n}\n\n.searchIcon {\n  pointer-events: none;\n\n  position: absolute;\n  z-index: 1;\n  left: 4px;\n\n  width: 16px;\n  height: 16px;\n\n  color: var(--color-text-secondary);\n\n  transition: color 200ms ease;\n}\n\n.input {\n  width: 100%;\n  padding: 4px 8px 4px 24px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background: transparent;\n  outline: none;\n\n  transition: all 200ms ease;\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.dropdownIcon {\n  cursor: pointer;\n\n  width: 20px;\n  height: 20px;\n  margin-left: 4px;\n\n  color: var(--color-text-secondary);\n\n  transition: transform 200ms ease;\n}\n\n.dropdownIcon:hover {\n  color: var(--color-text-primary);\n}\n\n.dropdownIconOpen {\n  transform: rotate(180deg);\n}\n\n.dropdown {\n  position: absolute;\n  z-index: 50;\n  top: 100%;\n  left: 0;\n\n  overflow-y: auto;\n\n  width: 100%;\n  max-height: 200px;\n  margin-top: 4px;\n  padding: 4px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n\n  animation: dropdown-show 200ms ease-out;\n}\n\n.option {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n\n  padding: 8px 12px;\n  border-radius: 4px;\n\n  transition: all 200ms ease;\n}\n\n.option:hover:not(.optionDisabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.optionFocused:not(.optionDisabled) {\n  color: var(--color-bg-primary);\n  background-color: var(--color-accent);\n}\n\n.optionDisabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.noResults {\n  padding: 12px;\n  text-align: center;\n}\n\n.noResultsText {\n  color: var(--color-text-secondary);\n}\n\n.errorMessage {\n  margin-top: 4px;\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.counter {\n  margin-top: 4px;\n  color: var(--color-text-secondary);\n}\n\n/* Animations */\n@keyframes dropdown-show {\n  from {\n    transform: translateY(-4px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Autocomplete/Autocomplete.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from '../Dialog'\nimport { Typography } from '../Typography'\nimport { Autocomplete, type AutocompleteOption } from './Autocomplete'\n\nconst meta = {\n  title: 'Components/Autocomplete',\n  component: Autocomplete,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Autocomplete>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n// Sample data\nconst programmingLanguages: AutocompleteOption[] = [\n  { value: 'javascript', label: 'JavaScript' },\n  { value: 'typescript', label: 'TypeScript' },\n  { value: 'python', label: 'Python' },\n  { value: 'java', label: 'Java' },\n  { value: 'cpp', label: 'C++' },\n  { value: 'csharp', label: 'C#' },\n  { value: 'php', label: 'PHP' },\n  { value: 'ruby', label: 'Ruby' },\n  { value: 'go', label: 'Go' },\n  { value: 'rust', label: 'Rust' },\n  { value: 'kotlin', label: 'Kotlin' },\n  { value: 'swift', label: 'Swift' },\n]\n\nconst musicGenres: AutocompleteOption[] = [\n  { value: 'rock', label: 'Rock' },\n  { value: 'pop', label: 'Pop' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hiphop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n  { value: 'blues', label: 'Blues' },\n  { value: 'reggae', label: 'Reggae' },\n  { value: 'folk', label: 'Folk' },\n  { value: 'metal', label: 'Metal' },\n  { value: 'indie', label: 'Indie' },\n]\n\nconst skills: AutocompleteOption[] = [\n  { value: 'frontend', label: 'Frontend Development' },\n  { value: 'backend', label: 'Backend Development' },\n  { value: 'fullstack', label: 'Full Stack Development' },\n  { value: 'mobile', label: 'Mobile Development' },\n  { value: 'devops', label: 'DevOps' },\n  { value: 'testing', label: 'Testing & QA' },\n  { value: 'design', label: 'UI/UX Design' },\n  { value: 'pm', label: 'Project Management', disabled: true },\n  { value: 'data', label: 'Data Science' },\n  { value: 'ml', label: 'Machine Learning' },\n]\n\nexport const Basic = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Programming Languages\"\n          placeholder=\"Search and select languages...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (max 3)\"\n          placeholder=\"Choose up to 3 genres...\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          maxTags={3}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithPreselected = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>(['javascript', 'typescript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Your Skills\"\n          placeholder=\"Add more skills...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithDisabledOptions = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Skills & Roles\"\n          placeholder=\"Select your skills...\"\n          options={skills}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>(['rock', 'jazz'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (disabled)\"\n          placeholder=\"Cannot select\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          disabled\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithError = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Required Skills\"\n          placeholder=\"Select at least one skill...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          errorMessage=\"Please select at least one programming language\"\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendSkills, setFrontendSkills] = useState<string[]>(['javascript'])\n    const [backendSkills, setBackendSkills] = useState<string[]>([])\n    const [genres, setGenres] = useState<string[]>([])\n\n    const frontendOptions: AutocompleteOption[] = [\n      { value: 'html', label: 'HTML' },\n      { value: 'css', label: 'CSS' },\n      { value: 'javascript', label: 'JavaScript' },\n      { value: 'typescript', label: 'TypeScript' },\n      { value: 'react', label: 'React' },\n      { value: 'vue', label: 'Vue.js' },\n      { value: 'angular', label: 'Angular' },\n      { value: 'svelte', label: 'Svelte' },\n    ]\n\n    const backendOptions: AutocompleteOption[] = [\n      { value: 'nodejs', label: 'Node.js' },\n      { value: 'python', label: 'Python' },\n      { value: 'java', label: 'Java' },\n      { value: 'csharp', label: 'C#' },\n      { value: 'php', label: 'PHP' },\n      { value: 'ruby', label: 'Ruby' },\n      { value: 'go', label: 'Go' },\n      { value: 'rust', label: 'Rust' },\n    ]\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Developer Profile Setup\n          </Typography>\n        </div>\n\n        <Autocomplete\n          label=\"Frontend Technologies\"\n          placeholder=\"Select frontend skills...\"\n          options={frontendOptions}\n          value={frontendSkills}\n          onChange={setFrontendSkills}\n          maxTags={5}\n        />\n\n        <Autocomplete\n          label=\"Backend Technologies\"\n          placeholder=\"Select backend skills...\"\n          options={backendOptions}\n          value={backendSkills}\n          onChange={setBackendSkills}\n          maxTags={4}\n        />\n\n        <Autocomplete\n          label=\"Favorite Music Genres\"\n          placeholder=\"What music do you like?\"\n          options={musicGenres}\n          value={genres}\n          onChange={setGenres}\n          maxTags={6}\n        />\n\n        <Card style={{ padding: '16px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Profile Summary\n          </Typography>\n\n          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>\n            <Typography variant=\"body2\">\n              <strong>Frontend:</strong>{' '}\n              {frontendSkills.length > 0 ? frontendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Backend:</strong>{' '}\n              {backendSkills.length > 0 ? backendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Music:</strong> {genres.length > 0 ? genres.join(', ') : 'None'}\n            </Typography>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => {\n    const [state1, setState1] = useState<string[]>([])\n    const [state2, setState2] = useState<string[]>(['rock', 'jazz'])\n    const [state3, setState3] = useState<string[]>([])\n    const [state4, setState4] = useState<string[]>(['javascript'])\n\n    return (\n      <div\n        style={{\n          width: '600px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '32px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Empty State\n          </Typography>\n          <Autocomplete\n            label=\"Programming Languages\"\n            placeholder=\"Start typing to search...\"\n            options={programmingLanguages}\n            value={state1}\n            onChange={setState1}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Selected Values\n          </Typography>\n          <Autocomplete\n            label=\"Music Genres\"\n            placeholder=\"Add more genres...\"\n            options={musicGenres}\n            value={state2}\n            onChange={setState2}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Error\n          </Typography>\n          <Autocomplete\n            label=\"Required Field\"\n            placeholder=\"This field is required\"\n            options={programmingLanguages}\n            value={state3}\n            onChange={setState3}\n            errorMessage=\"Please select at least one option\"\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Disabled State\n          </Typography>\n          <Autocomplete\n            label=\"Locked Selection\"\n            placeholder=\"Cannot modify\"\n            options={programmingLanguages}\n            value={state4}\n            onChange={setState4}\n            disabled\n          />\n        </div>\n      </div>\n    )\n  },\n}\n\nexport const InDialog = {\n  render: () => {\n    const [isOpen, setIsOpen] = useState(false)\n    const [selectedSkills, setSelectedSkills] = useState<string[]>([])\n    const [selectedGenres, setSelectedGenres] = useState<string[]>(['rock'])\n\n    const handleSubmit = () => {\n      console.log('Selected skills:', selectedSkills)\n      console.log('Selected genres:', selectedGenres)\n      setIsOpen(false)\n    }\n\n    const handleReset = () => {\n      setSelectedSkills([])\n      setSelectedGenres([])\n    }\n\n    return (\n      <>\n        <Button onClick={() => setIsOpen(true)}>Open Profile Settings</Button>\n\n        <Dialog open={isOpen} onClose={() => setIsOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Edit Your Profile</Typography>\n            <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n              Update your skills and music preferences\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '24px',\n                minWidth: '400px',\n              }}>\n              <Autocomplete\n                label=\"Technical Skills\"\n                placeholder=\"Search and select your skills...\"\n                options={skills}\n                value={selectedSkills}\n                onChange={setSelectedSkills}\n                maxTags={8}\n              />\n\n              <Autocomplete\n                label=\"Favorite Music Genres\"\n                placeholder=\"What music do you enjoy?\"\n                options={musicGenres}\n                value={selectedGenres}\n                onChange={setSelectedGenres}\n                maxTags={5}\n              />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset All\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setIsOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={handleSubmit}>\n              Save Profile\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Autocomplete/Autocomplete.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\n\nimport { useGetId } from '@/shared/hooks'\nimport { ArrowDownIcon, DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './Autocomplete.module.css'\n\nexport type AutocompleteOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type AutocompleteProps = {\n  label?: ReactNode\n  placeholder?: string\n  options: AutocompleteOption[]\n  value: string[]\n  onChange: (value: string[]) => void\n  disabled?: boolean\n  maxTags?: number\n  errorMessage?: string\n  className?: string\n} & Omit<ComponentProps<'div'>, 'onChange'>\n\nexport const Autocomplete = ({\n  label,\n  placeholder = 'Search and select...',\n  options,\n  value,\n  onChange,\n  disabled = false,\n  maxTags,\n  errorMessage,\n  className,\n  ...props\n}: AutocompleteProps) => {\n  const [isOpen, setIsOpen] = useState(false)\n  const [searchTerm, setSearchTerm] = useState('')\n  const [focusedIndex, setFocusedIndex] = useState(-1)\n\n  // For detecting clicks outside component to close dropdown\n  const containerRef = useRef<HTMLDivElement>(null)\n  // For programmatic focus management (Escape key, focus after selection)\n  const inputRef = useRef<HTMLInputElement>(null)\n\n  const id = useGetId(props.id)\n\n  const filteredOptions = options.filter(\n    (option) =>\n      option.label.toLowerCase().includes(searchTerm.toLowerCase()) && !value.includes(option.value)\n  )\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n  const showError = Boolean(errorMessage)\n\n  // Close dropdown on outside click\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n        setIsOpen(false)\n        setFocusedIndex(-1)\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen])\n\n  // Handle keyboard navigation\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (disabled) return\n\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault()\n        if (!isOpen) {\n          setIsOpen(true)\n          setFocusedIndex(0)\n        } else {\n          setFocusedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : prev))\n        }\n        break\n\n      case 'ArrowUp':\n        e.preventDefault()\n        setFocusedIndex((prev) => (prev > 0 ? prev - 1 : 0))\n        break\n\n      case 'Enter':\n        e.preventDefault()\n        if (isOpen && focusedIndex >= 0 && filteredOptions[focusedIndex]) {\n          selectOption(filteredOptions[focusedIndex])\n        }\n        break\n\n      case 'Escape':\n        e.preventDefault()\n        setIsOpen(false)\n        setFocusedIndex(-1)\n        inputRef.current?.blur()\n        break\n\n      case 'Backspace':\n        if (!searchTerm && value.length > 0) {\n          removeTag(value[value.length - 1])\n        }\n        break\n    }\n  }\n\n  const selectOption = (option: AutocompleteOption) => {\n    if (option.disabled || isMaxTagsReached) return\n\n    onChange([...value, option.value])\n    setSearchTerm('')\n    setFocusedIndex(-1)\n    inputRef.current?.focus()\n  }\n\n  const removeTag = (tagValue: string) => {\n    onChange(value.filter((v) => v !== tagValue))\n  }\n\n  const handleInputFocus = () => {\n    if (!disabled) {\n      setIsOpen(true)\n    }\n  }\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setSearchTerm(e.target.value)\n    setIsOpen(true)\n    setFocusedIndex(-1)\n  }\n\n  const selectedOptions = options.filter((option) => value.includes(option.value))\n\n  return (\n    <div className={clsx(s.container, className)} ref={containerRef} {...props}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          className={clsx(s.label, showError && s.labelError)}\n          as=\"label\"\n          htmlFor={id}>\n          {label}\n        </Typography>\n      )}\n\n      <div\n        className={clsx(\n          s.inputWrapper,\n          isOpen && s.focused,\n          showError && s.error,\n          disabled && s.disabled\n        )}>\n        {/* Selected tags */}\n        {selectedOptions.map((option) => (\n          <div key={option.value} className={s.tag}>\n            <Typography variant=\"body2\" className={s.tagText} as=\"label\">\n              {option.label}\n            </Typography>\n            {!disabled && (\n              <IconButton\n                onClick={() => removeTag(option.value)}\n                className={s.deleteButton}\n                aria-label={`Remove ${option.label}`}\n                type=\"button\"\n                tabIndex={-1}>\n                <DeleteIcon />\n              </IconButton>\n            )}\n          </div>\n        ))}\n\n        {/* Search input */}\n        <div className={s.inputContainer}>\n          <input\n            id={id}\n            ref={inputRef}\n            type=\"text\"\n            className={s.input}\n            value={searchTerm}\n            onChange={handleInputChange}\n            onFocus={handleInputFocus}\n            onKeyDown={handleKeyDown}\n            placeholder={value.length === 0 ? placeholder : ''}\n            disabled={disabled || isMaxTagsReached}\n            autoComplete=\"off\"\n          />\n        </div>\n\n        {/* Dropdown arrow */}\n        <ArrowDownIcon\n          className={clsx(s.dropdownIcon, isOpen && s.dropdownIconOpen)}\n          onClick={() => !disabled && setIsOpen(!isOpen)}\n        />\n      </div>\n\n      {/* Dropdown */}\n      {isOpen && !disabled && (\n        <div className={s.dropdown}>\n          {filteredOptions.length > 0 ? (\n            filteredOptions.map((option, index) => (\n              <div\n                key={option.value}\n                className={clsx(\n                  s.option,\n                  index === focusedIndex && s.optionFocused,\n                  option.disabled && s.optionDisabled\n                )}\n                onClick={() => !option.disabled && selectOption(option)}\n                onMouseEnter={() => setFocusedIndex(index)}>\n                <Typography variant=\"body2\">{option.label}</Typography>\n              </div>\n            ))\n          ) : (\n            <div className={s.noResults}>\n              <Typography variant=\"body2\" className={s.noResultsText}>\n                {searchTerm ? 'No options found' : 'All options selected'}\n              </Typography>\n            </div>\n          )}\n        </div>\n      )}\n\n      {/* Error message */}\n      {showError && (\n        <Typography variant=\"error\" className={s.errorMessage}>\n          {errorMessage}\n        </Typography>\n      )}\n\n      {/* Tags counter */}\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} selected\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Autocomplete/index.ts",
    "content": "export * from './Autocomplete'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Button/Button.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: inline-flex;\n  gap: 4px;\n  align-items: center;\n  justify-content: center;\n\n  height: 40px;\n  padding: 8px 16px;\n  border-radius: 45px;\n\n  font-size: var(--font-size-s);\n  font-weight: 600;\n  color: var(--color-text-primary);\n\n  transition: opacity 200ms;\n}\n\n.button:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.button:disabled {\n  cursor: initial;\n  background-color: var(--color-disabled);\n}\n\n.button:hover:not(:disabled),\n.button:focus:not(:disabled) {\n  opacity: 0.8;\n}\n\n.primary {\n  background-color: var(--color-accent);\n}\n\n.secondary {\n  background-color: var(--color-bg-interactive-secondary);\n}\n\n.fullWidth {\n  width: 100%;\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Button/Button.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Button } from './Button'\n\nconst meta = {\n  title: 'Components/Button',\n  component: Button,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Button>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllButtons: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '24px',\n        flexDirection: 'column',\n        alignItems: 'center',\n        width: '250px',\n      }}>\n      <Button variant=\"primary\">Primary</Button>\n      <Button variant=\"secondary\">Secondary</Button>\n      <Button fullWidth>Full Width</Button>\n      <Button disabled>Disabled</Button>\n      <Button variant=\"primary\" as=\"p\" href=\"https://it-incubator.io/\" target=\"_blank\">\n        Link\n      </Button>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Button/Button.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Button.module.css'\n\nexport type ButtonVariant = 'primary' | 'secondary'\n\nexport type ButtonProps<T extends ElementType = 'button'> = {\n  as?: T\n  fullWidth?: boolean\n  variant?: ButtonVariant\n} & ComponentProps<T>\n\nexport const Button = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  className,\n  fullWidth = false,\n  variant = 'primary',\n  ...props\n}: ButtonProps<T>) => {\n  const classNames = clsx(s.button, s[variant], fullWidth && s.fullWidth, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Button/index.ts",
    "content": "export * from './Button'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Card/Card.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  padding: 8px;\n  background: var(--color-bg-card);\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Card/Card.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from '../Typography'\nimport { Card } from './Card'\n\nconst meta = {\n  title: 'Components/Card',\n  component: Card,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Card>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  render: () => (\n    <Card>\n      <Typography variant=\"h2\">Chill Mix</Typography>\n      <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n        Julia Wolf, Khalid, ayokay and more\n      </Typography>\n    </Card>\n  ),\n}\n\nexport const AsSection: Story = {\n  render: () => (\n    <Card as=\"section\">\n      <Typography variant=\"h3\">Card as section</Typography>\n      <Typography variant=\"caption\">You can use any tag via 'as' prop</Typography>\n    </Card>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Card/Card.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType, ReactNode } from 'react'\n\nimport s from './Card.module.css'\n\nexport type CardProps<T extends ElementType = 'div'> = {\n  as?: T\n  className?: string\n  children?: ReactNode\n} & ComponentProps<T>\n\nexport const Card = <T extends ElementType = 'div'>({\n  as: Component = 'div',\n  className,\n  children,\n  ...props\n}: CardProps<T>) => {\n  return (\n    <Component className={clsx(s.card, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Card/index.ts",
    "content": "export * from './Card'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Dialog/Dialog.module.css",
    "content": ".backdrop {\n  position: fixed;\n  z-index: 1;\n  inset: 0;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  background-color: rgb(0 0 0 / 50%);\n\n  animation: fade-in 200ms ease-out;\n}\n\n.dialog {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n\n  max-width: 745px;\n  max-height: 90vh;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-secondary);\n\n  animation: slide-in 200ms ease-out;\n}\n\n.header {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  padding: 18px 24px;\n}\n\n.closeButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: 16px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.closeButton:hover {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  overflow-y: auto;\n  flex: 1;\n  padding: 20px 24px;\n}\n\n.footer {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 8px;\n  padding: 18px 24px;\n}\n\n/* Animations */\n@keyframes fade-in {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes slide-in {\n  from {\n    transform: translateY(-500px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n/* Responsive */\n@media (width <= 768px) {\n  .dialog {\n    max-width: 95vw;\n    margin: 20px;\n  }\n\n  .header,\n  .content,\n  .footer {\n    padding-right: 16px;\n    padding-left: 16px;\n  }\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Dialog/Dialog.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from './index'\n\nconst meta = {\n  title: 'Components/Dialog',\n  component: Dialog,\n  parameters: {\n    layout: 'centered',\n  },\n} satisfies Meta<typeof Dialog>\n\nexport default meta\n\nexport const BasicDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Open Basic Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Dialog Title</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <Typography variant=\"body1\">\n              This is dialog content. Here can be any content - text, forms, images and much more.\n            </Typography>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Confirm\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const FormDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Form Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Sign in to Spotifun</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '16px',\n                minWidth: '320px',\n              }}>\n              <TextField label=\"Email or username\" placeholder=\"Enter email or username\" />\n              <TextField label=\"Password\" type=\"password\" placeholder=\"Enter password\" />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Continue without signing in\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Sign in with API/HUB\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const WithoutCloseButton = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog without close button</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader showCloseButton={false}>\n            <Typography variant=\"h2\">Millions of songs.</Typography>\n            <Typography variant=\"body1\" style={{ color: 'var(--color-text-secondary)' }}>\n              Free on Musicfun.\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ textAlign: 'center', padding: '20px 0' }}>\n              <div\n                style={{\n                  width: '60px',\n                  height: '60px',\n                  borderRadius: '50%',\n                  backgroundColor: 'var(--color-accent)',\n                  margin: '0 auto 16px',\n                  display: 'flex',\n                  alignItems: 'center',\n                  justifyContent: 'center',\n                  fontSize: '24px',\n                }}>\n                😊\n              </div>\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '12px',\n                width: '100%',\n              }}>\n              <Button variant=\"primary\" fullWidth onClick={() => setOpen(false)}>\n                Sign up with API/HUB\n              </Button>\n              <Button variant=\"secondary\" fullWidth onClick={() => setOpen(false)}>\n                Continue without signing in\n              </Button>\n            </div>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const LongContent = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog with long content</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Long Content</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ maxWidth: '500px' }}>\n              {Array.from({ length: 20 }, (_, i) => (\n                <Typography key={i} variant=\"body2\" style={{ marginBottom: '12px' }}>\n                  This is paragraph number {i + 1}. Lorem ipsum dolor sit amet, consectetur\n                  adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna\n                  aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\n                </Typography>\n              ))}\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Close\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Dialog/Dialog.tsx",
    "content": "import { clsx } from 'clsx'\nimport { createContext, type ReactNode, use, useEffect } from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { IconButton } from '../IconButton'\nimport s from './Dialog.module.css'\n\ntype DialogContextType = {\n  onClose?: () => void\n}\n\nconst DialogContext = createContext<DialogContextType | null>(null)\n\nconst useDialogContext = () => {\n  const context = use(DialogContext)\n  if (!context) {\n    throw new Error('Dialog compound components must be used within Dialog component')\n  }\n  return context\n}\n\nexport type DialogProps = {\n  children: ReactNode\n  open: boolean\n  onClose?: () => void\n  className?: string\n}\n\nexport const Dialog = ({ children, open, onClose, className }: DialogProps) => {\n  const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (e.target === e.currentTarget) {\n      onClose?.()\n    }\n  }\n\n  // Add global keydown handler for ESC key\n  useEffect(() => {\n    if (!open) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose?.()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [open, onClose])\n\n  if (!open) return null\n\n  const dialogContent = (\n    <div className={s.backdrop} onClick={handleBackdropClick} role=\"dialog\" aria-modal=\"true\">\n      <section className={clsx(s.dialog, className)}>\n        <DialogContext value={{ onClose }}>{children}</DialogContext>\n      </section>\n    </div>\n  )\n\n  return createPortal(dialogContent, document.body)\n}\n\n/*\n * DialogHeader\n */\n\nexport type DialogHeaderProps = {\n  children?: ReactNode\n  className?: string\n  showCloseButton?: boolean\n}\n\nexport const DialogHeader = ({\n  children,\n  className,\n  showCloseButton = true,\n}: DialogHeaderProps) => {\n  const { onClose } = useDialogContext()\n\n  return (\n    <header className={clsx(s.header, className)}>\n      <div>{children}</div>\n      {showCloseButton && (\n        <IconButton onClick={onClose} aria-label=\"Close dialog\" type=\"button\">\n          ✕\n        </IconButton>\n      )}\n    </header>\n  )\n}\n\n/*\n * DialogContent\n */\n\nexport type DialogContentProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogContent = ({ children, className }: DialogContentProps) => {\n  return <div className={clsx(s.content, className)}>{children}</div>\n}\n\n/*\n * DialogFooter\n */\n\nexport type DialogFooterProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogFooter = ({ children, className }: DialogFooterProps) => {\n  return <footer className={clsx(s.footer, className)}>{children}</footer>\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Dialog/index.ts",
    "content": "export * from './Dialog'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/DropdownMenu/DropdownMenu.module.css",
    "content": ".container {\n  position: relative;\n  display: inline-block;\n}\n\n.trigger {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.trigger:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.trigger:enabled:hover,\n.trigger:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  position: fixed;\n  z-index: 50;\n\n  min-width: 160px;\n  padding: 4px;\n  border-radius: 8px;\n\n  background-color: var(--color-bg-primary);\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n}\n\n.content.align-start {\n  transform-origin: top left;\n}\n\n.content.align-center {\n  transform-origin: top center;\n  transform: translateX(-50%);\n}\n\n.content.align-end {\n  transform-origin: top right;\n  transform: translateX(-100%);\n}\n\n.content.side-top {\n  transform-origin: bottom;\n}\n\n.content.side-top.align-center {\n  transform: translateX(-50%) translateY(-100%);\n}\n\n.content.side-top.align-end {\n  transform: translateX(-100%) translateY(-100%);\n}\n\n.content.side-top.align-start {\n  transform: translateY(-100%);\n}\n\n.item {\n  cursor: pointer;\n\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: none;\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-align: left;\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.item:focus-visible {\n  background-color: var(--color-accent);\n  outline: none;\n}\n\n.item:hover:not(:disabled) {\n  background-color: var(--color-accent);\n}\n\n.itemDisabled {\n  cursor: not-allowed;\n  color: var(--color-text-secondary);\n  opacity: 0.5;\n}\n\n.itemDisabled:hover {\n  background: transparent;\n}\n\n.separator {\n  height: 1px;\n  margin: 4px 0;\n  background-color: var(--color-border-base);\n}\n\n/* Animations */\n@keyframes dropdown-menu-show {\n  from {\n    transform: scale(0.95);\n    opacity: 0;\n  }\n\n  to {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { CreateIcon, MoreIcon, PlusIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from './DropdownMenu'\n\nconst meta: Meta<typeof DropdownMenu> = {\n  title: 'Components/DropdownMenu',\n  component: DropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const BasicDropdownMenu: Story = {\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n          Show text song\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithIcons: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist')}>\n          <PlusIcon />\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithDisabledItem: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem disabled>Add to playlist (disabled)</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const CustomTrigger: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <IconButton aria-label=\"More options\">\n          <MoreIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Action 1')}>Action 1</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 2')}>Action 2</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 3')}>Action 3</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const DifferentAlignments: Story = {\n  args: {},\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '100px',\n        padding: '100px',\n        alignItems: 'center',\n        backgroundColor: 'var(--color-bg-secondary)',\n      }}>\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align Start\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align Center\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"center\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align End (default)\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </div>\n  ),\n}\n\nexport const WithLinks: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          as=\"a\"\n          href=\"https://example.com\"\n          target=\"_blank\"\n          onClick={() => console.log('Link clicked')}>\n          <PlusIcon />\n          Visit Website\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const Interactive: Story = {\n  args: {},\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '20px',\n        alignItems: 'center',\n        padding: '40px',\n      }}>\n      <Typography variant=\"h3\">Click the menu buttons to test functionality</Typography>\n\n      <div style={{ display: 'flex', gap: '20px' }}>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent>\n            <DropdownMenuItem onClick={() => console.log('Edit clicked')}>\n              <CreateIcon />\n              Edit track\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Add to playlist clicked')}>\n              <PlusIcon />\n              Add to playlist\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem\n              as=\"a\"\n              href=\"https://example.com\"\n              target=\"_blank\"\n              onClick={() => console.log('External link clicked')}>\n              Show lyrics online\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Download clicked')}>\n              Download\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem disabled>Share (coming soon)</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <IconButton aria-label=\"Playlist options\">\n              <MoreIcon />\n            </IconButton>\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem onClick={() => console.log('Edit playlist')}>\n              Edit playlist\n            </DropdownMenuItem>\n            <DropdownMenuItem\n              as=\"a\"\n              href=\"/share/playlist\"\n              onClick={() => console.log('Share playlist')}>\n              Share playlist\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Delete playlist')}>\n              Delete playlist\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <Typography variant=\"caption\">Open browser console to see click events</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/DropdownMenu/DropdownMenu.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  createContext,\n  type ElementType,\n  type ReactNode,\n  use,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\n\nimport s from './DropdownMenu.module.css'\n\ntype DropdownMenuContextType = {\n  isOpen: boolean\n  onClose: () => void\n  onToggle: () => void\n  triggerRef: React.RefObject<HTMLElement | null>\n}\n\nconst DropdownMenuContext = createContext<DropdownMenuContextType | null>(null)\n\nconst useDropdownMenuContext = () => {\n  const context = use(DropdownMenuContext)\n  if (!context) {\n    throw new Error('DropdownMenu compound components must be used within DropdownMenu component')\n  }\n  return context\n}\n\n/*\n * DropdownMenu\n */\n\nexport type DropdownMenuProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DropdownMenu = ({ children, className }: DropdownMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false)\n  const triggerRef = useRef<HTMLElement>(null)\n\n  const onClose = () => setIsOpen(false)\n  const onToggle = () => setIsOpen(!isOpen)\n\n  useBlockScroll({ isOpen, triggerRef })\n\n  // Close on escape key\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n    return () => document.removeEventListener('keydown', handleKeyDown)\n  }, [isOpen])\n\n  // Close on click outside\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as Element\n      if (\n        triggerRef.current &&\n        !triggerRef.current.contains(target) &&\n        !target.closest('[data-dropdown-content]')\n      ) {\n        onClose()\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen])\n\n  const contextValue = {\n    isOpen,\n    onClose,\n    onToggle,\n    triggerRef,\n  }\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <DropdownMenuContext value={contextValue}>{children}</DropdownMenuContext>\n    </div>\n  )\n}\n\n/*\n * DropdownMenuTrigger\n */\n\nexport type DropdownMenuTriggerProps = {\n  children: ReactNode\n  className?: string\n  asChild?: boolean\n}\n\nexport const DropdownMenuTrigger = ({\n  children,\n  className,\n  asChild = false,\n}: DropdownMenuTriggerProps) => {\n  const { onToggle, triggerRef } = useDropdownMenuContext()\n\n  if (asChild) {\n    return (\n      <div\n        ref={triggerRef as React.RefObject<HTMLDivElement>}\n        onClick={onToggle}\n        className={className}>\n        {children}\n      </div>\n    )\n  }\n\n  return (\n    <button\n      ref={triggerRef as React.RefObject<HTMLButtonElement>}\n      type=\"button\"\n      onClick={onToggle}\n      className={clsx(s.trigger, className)}>\n      {children}\n    </button>\n  )\n}\n\n/*\n * DropdownMenuContent\n */\n\nexport type DropdownMenuContentProps = {\n  children: ReactNode\n  className?: string\n  align?: 'start' | 'center' | 'end'\n  side?: 'top' | 'bottom' | 'left' | 'right'\n}\n\nexport const DropdownMenuContent = ({\n  children,\n  className,\n  align = 'end',\n  side = 'bottom',\n}: DropdownMenuContentProps) => {\n  const { isOpen, triggerRef } = useDropdownMenuContext()\n  const [position, setPosition] = useState({ top: 0, left: 0 })\n\n  // it's needed to prevent flickering\n  const [isPositioned, setIsPositioned] = useState(false)\n\n  useEffect(() => {\n    if (!isOpen || !triggerRef.current) {\n      setIsPositioned(false)\n      return\n    }\n\n    const triggerRect = triggerRef.current.getBoundingClientRect()\n    const scrollTop = window.pageYOffset || document.documentElement.scrollTop\n    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft\n\n    let top = 0\n    let left = 0\n\n    // Calculate position based on side\n    switch (side) {\n      case 'bottom':\n        top = triggerRect.bottom + scrollTop + 4\n        break\n      case 'top':\n        top = triggerRect.top + scrollTop - 4\n        break\n      case 'right':\n        left = triggerRect.right + scrollLeft + 4\n        top = triggerRect.top + scrollTop\n        break\n      case 'left':\n        left = triggerRect.left + scrollLeft - 4\n        top = triggerRect.top + scrollTop\n        break\n    }\n\n    // Calculate position based on align\n    if (side === 'bottom' || side === 'top') {\n      switch (align) {\n        case 'start':\n          left = triggerRect.left + scrollLeft\n          break\n        case 'center':\n          left = triggerRect.left + scrollLeft + triggerRect.width / 2\n          break\n        case 'end':\n          left = triggerRect.right + scrollLeft\n          break\n      }\n    }\n\n    setPosition({ top, left })\n    setIsPositioned(true)\n  }, [isOpen, align, side])\n\n  if (!isOpen || !isPositioned) return null\n\n  const content = (\n    <div\n      className={clsx(s.content, s[`align-${align}`], s[`side-${side}`], className)}\n      style={{ top: position.top, left: position.left }}\n      data-dropdown-content\n      role=\"menu\">\n      {children}\n    </div>\n  )\n\n  return createPortal(content, document.body)\n}\n\n/*\n * DropdownMenuItem\n */\n\nexport type DropdownMenuItemProps<T extends ElementType = 'button'> = {\n  as?: T\n  children: ReactNode\n  onClick?: () => void\n  className?: string\n  disabled?: boolean\n} & ComponentProps<T>\n\nexport const DropdownMenuItem = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  onClick,\n  className,\n  disabled = false,\n  ...props\n}: DropdownMenuItemProps<T>) => {\n  const { onClose } = useDropdownMenuContext()\n\n  const handleClick = () => {\n    if (disabled) return\n    onClick?.()\n    onClose()\n  }\n\n  const isButton = Component === 'button'\n\n  return (\n    <Component\n      {...(isButton && { type: 'button' })}\n      className={clsx(s.item, disabled && s.itemDisabled, className)}\n      onClick={handleClick}\n      {...(isButton && { disabled })}\n      role=\"menuitem\"\n      {...props}>\n      {children}\n    </Component>\n  )\n}\n\n/*\n * DropdownMenuSeparator\n */\n\nexport type DropdownMenuSeparatorProps = {\n  className?: string\n}\n\nexport const DropdownMenuSeparator = ({ className }: DropdownMenuSeparatorProps) => {\n  return <div className={clsx(s.separator, className)} role=\"separator\" />\n}\n\n/**\n * Block scroll when menu is open.\n */\nconst useBlockScroll = ({\n  isOpen,\n  triggerRef,\n}: {\n  isOpen: boolean\n  triggerRef: React.RefObject<HTMLElement | null>\n}) => {\n  // Block scroll when menu is open\n  useEffect(() => {\n    if (!isOpen || !triggerRef.current) return\n\n    const originalScrollElements: Array<{ element: Element; overflow: string }> = []\n\n    // Find all scrollable parent elements\n    const findScrollableParents = (element: Element) => {\n      const scrollableElements: Element[] = []\n      let parent = element.parentElement\n\n      while (parent && parent !== document.body) {\n        const style = window.getComputedStyle(parent)\n        const hasVerticalScroll =\n          style.overflowY === 'auto' ||\n          style.overflowY === 'scroll' ||\n          style.overflow === 'auto' ||\n          style.overflow === 'scroll'\n\n        if (hasVerticalScroll && parent.scrollHeight > parent.clientHeight) {\n          scrollableElements.push(parent)\n        }\n        parent = parent.parentElement\n      }\n\n      return scrollableElements\n    }\n\n    // Block scroll on body\n    const bodyOverflow = document.body.style.overflow\n    document.body.style.overflow = 'hidden'\n    originalScrollElements.push({ element: document.body, overflow: bodyOverflow })\n\n    // Block scroll on scrollable parents\n    const scrollableParents = findScrollableParents(triggerRef.current)\n    scrollableParents.forEach((element) => {\n      const originalOverflow = (element as HTMLElement).style.overflow\n      ;(element as HTMLElement).style.overflow = 'hidden'\n      originalScrollElements.push({ element, overflow: originalOverflow })\n    })\n\n    return () => {\n      // Restore original overflow values\n      originalScrollElements.forEach(({ element, overflow }) => {\n        ;(element as HTMLElement).style.overflow = overflow\n      })\n    }\n  }, [isOpen])\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/DropdownMenu/index.ts",
    "content": "export * from './DropdownMenu'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Hashtag/Tag.module.css",
    "content": ".hashtag {\n  cursor: pointer;\n\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n\n  min-width: 73px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 45px;\n\n  font-size: var(--font-size-xxxs);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  text-decoration: none;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.hashtag:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.hashtag:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.active {\n  color: var(--color-bg-primary);\n  background-color: var(--color-text-primary);\n}\n\n.active:hover:not(:disabled) {\n  color: var(--color-bg-primary);\n  opacity: 0.9;\n  background-color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Hashtag/Tag.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Tag } from './Tag.tsx'\n\nconst meta = {\n  title: 'Components/Hashtag',\n  component: Tag,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    tag: 'Playlists',\n  },\n} satisfies Meta<typeof Tag>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n\nexport const Active: Story = {\n  args: {\n    active: true,\n  },\n}\n\nexport const AsLink: Story = {\n  args: {\n    as: 'a',\n    href: 'https://www.google.com',\n    target: '_blank',\n  },\n}\n\nexport const AllHashtags: Story = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '16px' }}>\n      <Tag tag=\"Playlists\" />\n      <Tag active tag=\"Artists\" />\n      <Tag tag=\"Albums\" />\n      <Tag as=\"a\" href=\"#\" tag=\"Podcasts & shows\">\n        Podcasts & shows\n      </Tag>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Hashtag/Tag.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Tag.module.css'\n\nexport type HashtagProps<T extends ElementType = 'button'> = {\n  as?: T\n  active?: boolean\n  tag: string\n  className?: string\n} & ComponentProps<T>\n\nexport const Tag = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  active = false,\n  tag,\n  className,\n  ...props\n}: HashtagProps<T>) => {\n  const classNames = clsx(s.hashtag, active && s.active, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      #{tag}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Hashtag/index.ts",
    "content": "export * from './Tag.tsx'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/IconButton/IconButton.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.button:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.button:enabled:hover,\n.button:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/IconButton/IconButton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport {\n  DownloadIcon,\n  HomeIcon,\n  LikeIcon,\n  MoreIcon,\n  PlayIcon,\n  PlusIcon,\n  SearchIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from './IconButton'\n\nconst meta = {\n  title: 'Components/IconButton',\n  component: IconButton,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof IconButton>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    children: <PlayIcon />,\n    'aria-label': 'Play',\n  },\n}\n\nexport const AllIcons = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '16px',\n        flexWrap: 'wrap',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: '20px',\n      }}>\n      <IconButton aria-label=\"Home\">\n        <HomeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Search\">\n        <SearchIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Play\">\n        <PlayIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Like\">\n        <LikeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Add\">\n        <PlusIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"More options\">\n        <MoreIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Download\">\n        <DownloadIcon />\n      </IconButton>\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    children: <PlayIcon />,\n    disabled: true,\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/IconButton/IconButton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './IconButton.module.css'\n\ntype IconButtonProps = {\n  children: React.ReactNode\n} & ComponentProps<'button'>\n\nexport const IconButton = ({ children, className, ...props }: IconButtonProps) => {\n  return (\n    <button type=\"button\" className={clsx(s.button, className)} {...props}>\n      {children}\n    </button>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/IconButton/index.ts",
    "content": "export * from './IconButton'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/ImageUploader/ImageUploader.module.css",
    "content": ".container {\n  width: 100%;\n}\n\n.dropZone {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 100%;\n  min-height: 280px;\n  border: 2px dashed var(--color-border-input-primary);\n  border-radius: 8px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover,\n.dropZone:focus-within {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.dragOver {\n  border-color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.hasPreview {\n  border-color: var(--color-border-input-active);\n  border-style: solid;\n}\n\n.dropZone.error {\n  border-color: var(--color-text-error);\n}\n\n.hiddenInput {\n  position: absolute;\n\n  overflow: hidden;\n\n  width: 1px;\n  height: 1px;\n\n  opacity: 0;\n  clip: rect(0, 0, 0, 0);\n}\n\n.uploadContent {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  align-items: center;\n\n  padding: 32px 16px;\n}\n\n.uploadIcon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n\n  color: var(--color-text-secondary);\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover .uploadIcon,\n.dropZone:focus-within .uploadIcon {\n  color: var(--color-accent);\n  background-color: var(--color-bg-card);\n}\n\n.uploadText {\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  transition: color 200ms ease;\n}\n\n.dropZone:hover .uploadText {\n  color: var(--color-text-primary);\n}\n\n.previewContainer {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.previewImage {\n  width: 100%;\n  height: 100%;\n  min-height: 200px;\n  border-radius: 6px;\n\n  object-fit: cover;\n}\n\n.removeButton {\n  position: absolute;\n  top: 8px;\n  right: 8px;\n}\n\n.removeButton:hover {\n  opacity: 1;\n  background-color: var(--color-text-error);\n}\n\n.removeButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.errorMessage {\n  margin-top: 8px;\n}\n\n/* States for different sizes */\n.dropZone.small {\n  min-height: 120px;\n}\n\n.dropZone.large {\n  min-height: 300px;\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/ImageUploader/ImageUploader.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ImageUploader } from './ImageUploader'\n\nconst meta = {\n  title: 'Components/ImageUploader',\n  component: ImageUploader,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    onImageSelect: () => {},\n  },\n} satisfies Meta<typeof ImageUploader>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    placeholder: 'Upload Cover Image',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const CustomPlaceholder: Story = {\n  args: {\n    placeholder: 'Choose your avatar',\n  },\n  render: (args) => (\n    <div style={{ width: '200px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const WithCustomLimits: Story = {\n  args: {\n    placeholder: 'Upload image (max 2MB)',\n    maxSizeInMB: 2,\n    acceptedFormats: ['image/jpeg', 'image/png'],\n  },\n  render: (args) => (\n    <div style={{ width: '400px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const AllowAllImages: Story = {\n  args: {\n    placeholder: 'Upload any image format',\n    acceptedFormats: ['image/*'],\n    maxSizeInMB: 10,\n  },\n  render: (args) => (\n    <div style={{ width: '350px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const Interactive: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '400px',\n      }}>\n      <div>\n        <h3 style={{ color: 'var(--color-text-primary)', marginBottom: '12px' }}>Profile Avatar</h3>\n        <ImageUploader\n          placeholder=\"Upload avatar\"\n          onImageSelect={(file) => console.log('Avatar selected:', file.name)}\n          maxSizeInMB={1}\n        />\n      </div>\n\n      <div>\n        <h3 style={{ color: 'var(--color-text-primary)', marginBottom: '12px' }}>Playlist Cover</h3>\n        <ImageUploader\n          placeholder=\"Upload Cover Image\"\n          onImageSelect={(file) => console.log('Cover selected:', file.name)}\n          maxSizeInMB={5}\n        />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/ImageUploader/ImageUploader.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ChangeEvent, type DragEvent, useRef, useState } from 'react'\n\nimport { ImageUploadIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './ImageUploader.module.css'\n\nexport type ImageUploaderProps = {\n  onImageSelect: (file: File) => void\n  className?: string\n  acceptedFormats?: string[]\n  maxSizeInMB?: number\n  placeholder?: string\n}\n\nexport const ImageUploader = ({\n  className,\n  onImageSelect,\n  acceptedFormats = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'],\n  maxSizeInMB = 5,\n  placeholder = 'Upload Cover Image',\n}: ImageUploaderProps) => {\n  const [isDragOver, setIsDragOver] = useState(false)\n  const [preview, setPreview] = useState<string | null>(null)\n  const [error, setError] = useState<string | null>(null)\n  const fileInputRef = useRef<HTMLInputElement>(null)\n\n  const validateFile = (file: File): string | null => {\n    if (!acceptedFormats.includes(file.type)) {\n      return `Only ${acceptedFormats.join(', ')} files are allowed`\n    }\n\n    const maxSizeInBytes = maxSizeInMB * 1024 * 1024\n    if (file.size > maxSizeInBytes) {\n      return `File size must be less than ${maxSizeInMB}MB`\n    }\n\n    return null\n  }\n\n  const handleFileSelect = (file: File) => {\n    const validationError = validateFile(file)\n\n    if (validationError) {\n      setError(validationError)\n      setPreview(null)\n      return\n    }\n\n    setError(null)\n\n    // Create preview\n    const reader = new FileReader()\n    reader.onload = (e) => {\n      setPreview(e.target?.result as string)\n    }\n    reader.readAsDataURL(file)\n\n    onImageSelect(file)\n  }\n\n  const handleFileInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0]\n    if (file) {\n      handleFileSelect(file)\n    }\n  }\n\n  const handleDragOver = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(true)\n  }\n\n  const handleDragLeave = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n  }\n\n  const handleDrop = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n\n    const files = Array.from(e.dataTransfer.files)\n    const imageFile = files.find((file) => file.type.startsWith('image/'))\n\n    if (imageFile) {\n      handleFileSelect(imageFile)\n    }\n  }\n\n  const handleRemoveImage = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    setPreview(null)\n    setError(null)\n    // Clear input value to allow selecting the same file again\n    if (fileInputRef.current) {\n      fileInputRef.current.value = ''\n    }\n  }\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <label\n        className={clsx(\n          s.dropZone,\n          isDragOver && s.dragOver,\n          preview && s.hasPreview,\n          error && s.error\n        )}\n        onDragOver={handleDragOver}\n        onDragLeave={handleDragLeave}\n        onDrop={handleDrop}>\n        <input\n          ref={fileInputRef}\n          type=\"file\"\n          accept={acceptedFormats.join(',')}\n          onChange={handleFileInputChange}\n          className={s.hiddenInput}\n          tabIndex={0}\n        />\n\n        {preview ? (\n          <div className={s.previewContainer}>\n            <img src={preview} alt=\"Preview\" className={s.previewImage} />\n            <IconButton\n              className={s.removeButton}\n              onClick={handleRemoveImage}\n              aria-label=\"Remove image\"\n              type=\"button\">\n              ✕\n            </IconButton>\n          </div>\n        ) : (\n          <div className={s.uploadContent}>\n            <div className={s.uploadIcon}>\n              <ImageUploadIcon width={24} height={24} />\n            </div>\n            <Typography variant=\"body2\" className={s.uploadText}>\n              {placeholder}\n            </Typography>\n          </div>\n        )}\n      </label>\n\n      {error && (\n        <Typography variant=\"error\" className={s.errorMessage}>\n          {error}\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/ImageUploader/index.ts",
    "content": "export * from './ImageUploader'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Pagination/Pagination.module.css",
    "content": ".pagination {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n}\n\n.navButton {\n  width: 40px;\n  height: 40px;\n  border-radius: 4px;\n\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.navButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n  background-color: var(--color-bg-secondary);\n}\n\n.navButton:enabled:hover,\n.navButton:enabled:focus {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageNumbers {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n}\n\n.pageButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n  border: none;\n  border-radius: 8px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.pageButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.pageButton:hover:not(.active) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageButton.active {\n  background-color: var(--color-accent);\n}\n\n.pageButton.active:hover {\n  opacity: 0.9;\n}\n\n.ellipsis {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* Responsive adjustments */\n@media (width <= 480px) {\n  .pagination {\n    gap: 2px;\n  }\n\n  .navButton,\n  .pageButton,\n  .ellipsis {\n    width: 36px;\n    height: 36px;\n  }\n\n  .pageButton,\n  .ellipsis {\n    font-size: var(--font-size-s);\n  }\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Pagination/Pagination.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Pagination } from './Pagination'\n\nconst meta = {\n  title: 'Components/Pagination',\n  component: Pagination,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Pagination>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    page: 1,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const MiddlePage: Story = {\n  args: {\n    page: 5,\n    pagesCount: 10,\n    onPageChange: () => {},\n  },\n}\n\nexport const LastPage: Story = {\n  args: {\n    page: 3,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const ManyPages: Story = {\n  args: {\n    page: 8,\n    pagesCount: 20,\n    onPageChange: () => {},\n  },\n}\n\nexport const SinglePage: Story = {\n  args: {\n    page: 1,\n    pagesCount: 1,\n    onPageChange: () => {},\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [currentPage, setCurrentPage] = useState(1)\n    const totalCount = 95\n    const pageSize = 10\n    const pagesCount = Math.ceil(totalCount / pageSize)\n\n    const handlePageChange = (page: number) => {\n      setCurrentPage(page)\n    }\n\n    return (\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n          alignItems: 'center',\n          width: '500px',\n        }}>\n        <Card style={{ padding: '20px', textAlign: 'center' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Interactive Pagination\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Current page: <strong>{currentPage}</strong>\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Total items: <strong>{totalCount}</strong>\n          </Typography>\n          <Typography variant=\"body2\">\n            Items per page: <strong>{pageSize}</strong>\n          </Typography>\n        </Card>\n\n        <Pagination page={currentPage} pagesCount={pagesCount} onPageChange={handlePageChange} />\n\n        <Typography variant=\"caption\" style={{ textAlign: 'center' }}>\n          Click on page numbers or arrows to navigate\n        </Typography>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '32px',\n        alignItems: 'center',\n        width: '600px',\n      }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          First Page (3 pages total)\n        </Typography>\n        <Pagination page={1} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Middle Page (10 pages total)\n        </Typography>\n        <Pagination page={5} pagesCount={10} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Last Page (3 pages total)\n        </Typography>\n        <Pagination page={3} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Many Pages (20 pages total)\n        </Typography>\n        <Pagination page={12} pagesCount={20} onPageChange={() => {}} />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Pagination/Pagination.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport { KeyboardArrowLeftIcon, KeyboardArrowRightIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './Pagination.module.css'\n\nexport type PaginationProps = {\n  page: number\n  pagesCount: number\n  onPageChange: (page: number) => void\n  className?: string\n} & Omit<ComponentProps<'div'>, 'children'>\n\nconst MAX_VISIBLE_PAGES = 5\n\nexport const Pagination = ({\n  page,\n\n  pagesCount,\n  onPageChange,\n  className,\n  ...props\n}: PaginationProps) => {\n  // Helper function to generate page numbers array\n  const generatePageNumbers = () => {\n    const pages: (number | 'ellipsis')[] = []\n\n    if (pagesCount <= MAX_VISIBLE_PAGES) {\n      // Show all pages if total is small\n      for (let i = 1; i <= pagesCount; i++) {\n        pages.push(i)\n      }\n    } else {\n      // Always show first page\n      pages.push(1)\n\n      if (page > 3) {\n        pages.push('ellipsis')\n      }\n\n      // Show pages around current page\n      const start = Math.max(2, page - 1)\n      const end = Math.min(pagesCount - 1, page + 1)\n\n      for (let i = start; i <= end; i++) {\n        if (i !== 1 && i !== pagesCount) {\n          pages.push(i)\n        }\n      }\n\n      if (page < pagesCount - 2) {\n        pages.push('ellipsis')\n      }\n\n      // Always show last page if it's not already included\n      if (pagesCount > 1) {\n        pages.push(pagesCount)\n      }\n    }\n\n    return pages\n  }\n\n  const handlePrevious = () => {\n    if (page > 1) {\n      onPageChange(page - 1)\n    }\n  }\n\n  const handleNext = () => {\n    if (page < pagesCount) {\n      onPageChange(page + 1)\n    }\n  }\n\n  const handlePageClick = (pageNumber: number) => {\n    onPageChange(pageNumber)\n  }\n\n  if (pagesCount <= 1) {\n    return null\n  }\n\n  const pageNumbers = generatePageNumbers()\n\n  return (\n    <div\n      className={clsx(s.pagination, className)}\n      role=\"navigation\"\n      aria-label=\"Pagination\"\n      {...props}>\n      {/* Previous button */}\n      <IconButton\n        onClick={handlePrevious}\n        disabled={page === 1}\n        aria-label=\"Go to previous page\"\n        className={s.navButton}>\n        <KeyboardArrowLeftIcon />\n      </IconButton>\n\n      {/* Page numbers */}\n      <div className={s.pageNumbers}>\n        {pageNumbers.map((pageNumber, index) => {\n          if (pageNumber === 'ellipsis') {\n            return (\n              <span key={`ellipsis-${index}`} className={s.ellipsis} aria-hidden=\"true\">\n                ...\n              </span>\n            )\n          }\n\n          const isActive = pageNumber === page\n\n          return (\n            <button\n              key={pageNumber}\n              onClick={() => handlePageClick(pageNumber)}\n              className={clsx(s.pageButton, isActive && s.active)}\n              aria-label={`Go to page ${pageNumber}`}\n              aria-current={isActive ? 'page' : undefined}\n              type=\"button\">\n              {pageNumber}\n            </button>\n          )\n        })}\n      </div>\n\n      {/* Next button */}\n      <IconButton\n        onClick={handleNext}\n        disabled={page === pagesCount}\n        aria-label=\"Go to next page\"\n        className={s.navButton}>\n        <KeyboardArrowRightIcon />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Pagination/index.ts",
    "content": "export * from './Pagination'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Progress/Progress.module.css",
    "content": ".progress {\n  overflow: hidden;\n\n  width: 100%;\n  height: 4px;\n  border-radius: 4px;\n\n  background-color: var(--color-border-base);\n}\n\n.progressBar {\n  height: 100%;\n  border-radius: 4px;\n  background-color: var(--color-accent);\n  transition: width 300ms ease;\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Progress/Progress.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Progress } from './Progress'\n\nconst meta = {\n  title: 'Components/Progress',\n  component: Progress,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Progress>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    value: 75,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const CustomMax: Story = {\n  args: {\n    value: 15,\n    max: 20,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Empty: Story = {\n  args: {\n    value: 0,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Full: Story = {\n  args: {\n    value: 100,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Empty (0%)\n        </Typography>\n        <Progress value={0} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Low (25%)\n        </Typography>\n        <Progress value={25} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Medium (50%)\n        </Typography>\n        <Progress value={50} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          High (85%)\n        </Typography>\n        <Progress value={85} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Complete (100%)\n        </Typography>\n        <Progress value={100} />\n      </div>\n    </div>\n  ),\n}\n\nexport const CustomSizes = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Small (height: 4px)\n        </Typography>\n        <Progress value={70} style={{ height: '4px' }} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Default (height: 8px)\n        </Typography>\n        <Progress value={70} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Large (height: 12px)\n        </Typography>\n        <Progress value={70} style={{ height: '12px' }} />\n      </div>\n    </div>\n  ),\n}\n\nexport const Interactive = {\n  render: () => {\n    const [progress, setProgress] = useState(0)\n\n    const handleIncrease = () => {\n      setProgress((prev) => Math.min(prev + 10, 100))\n    }\n\n    const handleDecrease = () => {\n      setProgress((prev) => Math.max(prev - 10, 0))\n    }\n\n    const handleReset = () => {\n      setProgress(0)\n    }\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Card style={{ padding: '24px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Interactive Progress\n          </Typography>\n\n          <div style={{ marginBottom: '16px' }}>\n            <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n              Current progress: {progress}%\n            </Typography>\n            <Progress value={progress} />\n          </div>\n\n          <div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>\n            <Button variant=\"secondary\" onClick={handleDecrease} disabled={progress === 0}>\n              -10%\n            </Button>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset\n            </Button>\n            <Button variant=\"primary\" onClick={handleIncrease} disabled={progress === 100}>\n              +10%\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const FileUploadExample = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Card style={{ padding: '24px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          File Upload Progress\n        </Typography>\n\n        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">image.jpg</Typography>\n              <Typography variant=\"body2\">75%</Typography>\n            </div>\n            <Progress value={75} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">document.pdf</Typography>\n              <Typography variant=\"body2\">100%</Typography>\n            </div>\n            <Progress value={100} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">video.mp4</Typography>\n              <Typography variant=\"body2\">32%</Typography>\n            </div>\n            <Progress value={32} />\n          </div>\n        </div>\n      </Card>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Progress/Progress.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './Progress.module.css'\n\nexport type ProgressProps = {\n  value: number\n  max?: number\n} & ComponentProps<'div'>\n\nexport const Progress = ({ value, max = 100, className, ...props }: ProgressProps) => {\n  const percentage = Math.min(Math.max((value / max) * 100, 0), 100)\n\n  return (\n    <div\n      className={clsx(s.progress, className)}\n      role=\"progressbar\"\n      aria-valuenow={value}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      {...props}>\n      <div className={s.progressBar} style={{ width: `${percentage}%` }} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Progress/index.ts",
    "content": "export * from './Progress'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/ReactionButtons/ReactionButtons.module.css",
    "content": ".container {\n  display: flex;\n  gap: 8px;\n  align-items: start;\n}\n\n.button {\n  width: 28px;\n  height: 28px;\n  padding: 0;\n  transition: color 200ms ease;\n}\n\n.button.large {\n  width: 40px;\n  height: 40px;\n}\n\n.button.liked {\n  color: var(--color-accent);\n}\n\n.button.disliked {\n  color: var(--color-accent);\n}\n\n.button:enabled:hover:is(.liked, .disliked),\n.button:enabled:focus:is(.liked, .disliked) {\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.likesCountBox {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.likesCount {\n  font-size: 10px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/ReactionButtons/ReactionButtons.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { type CurrentUserReaction, ReactionButtons } from './ReactionButtons'\n\nconst meta = {\n  title: 'Components/ReactionButtons',\n  component: ReactionButtons,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof ReactionButtons>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    reaction: 0,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n  },\n}\n\nexport const WithLikesCount: Story = {\n  args: {\n    reaction: 0,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n    likesCount: 10,\n  },\n}\n\nexport const LikedState: Story = {\n  args: {\n    reaction: 1,\n    onLike: () => console.log('Unlike'),\n    onDislike: () => console.log('Disliked!'),\n  },\n}\n\nexport const DislikedState: Story = {\n  args: {\n    reaction: -1,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Remove dislike'),\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [reaction, setReaction] = useState<CurrentUserReaction>(0)\n\n    const handleLike = () => {\n      setReaction(reaction === 1 ? 0 : 1)\n    }\n\n    const handleDislike = () => {\n      setReaction(reaction === -1 ? 0 : -1)\n    }\n\n    return (\n      <Card style={{ padding: '24px', maxWidth: '300px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          Interactive Reaction Buttons\n        </Typography>\n\n        <Typography variant=\"body2\" style={{ marginBottom: '16px' }}>\n          Try clicking the buttons below:\n        </Typography>\n\n        <div style={{ display: 'flex', justifyContent: 'center' }}>\n          <ReactionButtons reaction={reaction} onLike={handleLike} onDislike={handleDislike} />\n        </div>\n\n        <Typography\n          variant=\"caption\"\n          style={{ marginTop: '16px', textAlign: 'center', display: 'block' }}>\n          Status: {reaction === 1 ? '👍 Liked' : reaction === -1 ? '👎 Disliked' : '😐 Neutral'}\n        </Typography>\n      </Card>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Default\n        </Typography>\n        <ReactionButtons reaction={0} onLike={() => {}} onDislike={() => {}} />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Liked\n        </Typography>\n        <ReactionButtons reaction={1} onLike={() => {}} onDislike={() => {}} />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Disliked\n        </Typography>\n        <ReactionButtons reaction={-1} onLike={() => {}} onDislike={() => {}} />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          With likes count\n        </Typography>\n        <ReactionButtons reaction={0} onLike={() => {}} onDislike={() => {}} likesCount={10} />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/ReactionButtons/ReactionButtons.tsx",
    "content": "import { clsx } from 'clsx'\n\nimport type { components, SchemaReactionValue } from '@/shared/api/schema.ts'\nimport { DislikeIcon, LikeIcon, LikeIconFill } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './ReactionButtons.module.css'\n\n// duplication of the CurrentUserReaction type to decouple the shared layer from the features layer\nexport type CurrentUserReaction = SchemaReactionValue\n\nexport type ReactionButtonsProps = {\n  reaction?: CurrentUserReaction\n  onLike: () => void\n  onDislike: () => void\n  likesCount?: number\n  className?: string\n  size?: keyof typeof SIZE_MAP\n}\n\nconst SIZE_MAP = {\n  small: 28,\n  large: 40,\n}\n\nexport const ReactionButtons = ({\n  reaction = 0,\n  onLike,\n  onDislike,\n  likesCount,\n  className,\n  size = 'small',\n}: ReactionButtonsProps) => {\n  const isLiked = reaction === 1\n  const isDisliked = reaction === -1\n\n  const iconSize = SIZE_MAP[size]\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <div className={s.likesCountBox}>\n        <IconButton\n          onClick={(e) => {\n            e.preventDefault()\n            onLike()\n          }}\n          className={clsx(s.button, isLiked && s.liked, size === 'large' && s.large)}\n          aria-label={isLiked ? 'Remove like' : 'Like'}\n          type=\"button\">\n          {isLiked ? (\n            <LikeIconFill width={iconSize} height={iconSize} />\n          ) : (\n            <LikeIcon width={iconSize} height={iconSize} />\n          )}\n        </IconButton>\n        <span className={s.likesCount}>{likesCount}</span>\n      </div>\n\n      <IconButton\n        onClick={(e) => {\n          e.preventDefault()\n          onDislike()\n        }}\n        className={clsx(s.button, isDisliked && s.disliked, size === 'large' && s.large)}\n        aria-label={isDisliked ? 'Remove dislike' : 'Dislike'}\n        type=\"button\">\n        <DislikeIcon width={iconSize} height={iconSize} />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/ReactionButtons/index.ts",
    "content": "export * from './ReactionButtons'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/SearchField/SearchField.module.css",
    "content": ".inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.searchIcon {\n  pointer-events: none;\n\n  position: absolute;\n  z-index: 1;\n  left: 12px;\n\n  color: var(--color-text-secondary);\n\n  transition: color 200ms ease;\n}\n\n.input {\n  width: 100%;\n  height: 52px;\n  padding: 15px 16px 15px 62px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 26px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary-reverse);\n\n  background-color: var(--color-bg-primary-reverse);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input::placeholder {\n  font-size: var(--font-size-m);\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/SearchField/SearchField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchField } from './SearchField'\n\nconst meta = {\n  title: 'Components/SearchField',\n  component: SearchField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof SearchField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    placeholder: 'Search for playlists...',\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/SearchField/SearchField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport s from './SearchField.module.css'\n\nexport type SearchFieldProps = {\n  label?: ReactNode\n  placeholder?: string\n} & ComponentProps<'input'>\n\nexport const SearchField = ({\n  className,\n  placeholder = 'Search...',\n  ...props\n}: SearchFieldProps) => {\n  return (\n    <div className={clsx(s.inputWrapper, className)}>\n      <SearchIcon className={s.searchIcon} />\n      <input className={clsx(s.input)} type=\"text\" placeholder={placeholder} {...props} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/SearchField/index.ts",
    "content": "export * from './SearchField'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Select/Select.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.selectWrapper {\n  position: relative;\n  width: 100%;\n}\n\n.select {\n  width: 100%;\n  height: 40px;\n  padding: 8px 36px 8px 12px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-decoration: underline;\n  text-underline-offset: 3px;\n\n  appearance: none;\n  background-color: transparent;\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color;\n}\n\n.select:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.select:focus-visible {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select.error {\n  border-color: var(--color-text-error);\n}\n\n/* Style dropdown options */\n.select option {\n  padding: 8px 12px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: background-color 200ms ease;\n}\n\n.select option:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:checked {\n  font-weight: 600;\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:disabled {\n  color: var(--color-disabled);\n}\n\n/* Custom dropdown icon */\n.icon {\n  pointer-events: none;\n\n  position: absolute;\n  top: 50%;\n  right: 12px;\n  transform: translateY(-50%);\n\n  width: 20px;\n  height: 20px;\n\n  color: var(--color-text-secondary);\n\n  transition:\n    color 200ms ease,\n    transform 200ms ease;\n}\n\n/* Rotate icon when dropdown is open */\n.select:open + .icon {\n  transform: translateY(-50%) rotate(180deg);\n}\n\n.label.error {\n  color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Select/Select.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Select } from './Select'\n\nconst meta = {\n  title: 'Components/Select',\n  component: Select,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Select>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst commonOptions = [\n  { value: 'react', label: 'React' },\n  { value: 'vue', label: 'Vue.js' },\n  { value: 'angular', label: 'Angular' },\n  { value: 'svelte', label: 'Svelte' },\n  { value: 'vanilla', label: 'Vanilla JS' },\n]\n\nconst genres = [\n  { value: 'pop', label: 'Pop' },\n  { value: 'rock', label: 'Rock' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hip-hop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n]\n\nexport const AllVariants = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '350px',\n      }}>\n      <Select label=\"Basic Select\" placeholder=\"Choose option\" options={commonOptions} />\n\n      <Select label=\"With Default Value\" options={commonOptions} defaultValue=\"react\" />\n\n      <Select\n        label=\"With Error\"\n        placeholder=\"Choose option\"\n        options={commonOptions}\n        errorMessage=\"This field is required\"\n      />\n\n      <Select label=\"Disabled\" placeholder=\"Cannot select\" options={commonOptions} disabled />\n    </div>\n  ),\n}\n\nexport const Basic: Story = {\n  args: {\n    label: 'Choose framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDefaultValue: Story = {\n  args: {\n    label: 'Preferred framework',\n    options: commonOptions,\n    defaultValue: 'react',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Framework (disabled)',\n    placeholder: 'Cannot select',\n    options: commonOptions,\n    disabled: true,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithError: Story = {\n  args: {\n    label: 'Framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n    errorMessage: 'Please select a framework',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDisabledOptions: Story = {\n  args: {\n    label: 'Music Genre',\n    placeholder: 'Choose your favorite genre',\n    options: [\n      { value: 'pop', label: 'Pop' },\n      { value: 'rock', label: 'Rock' },\n      { value: 'jazz', label: 'Jazz (Coming Soon)', disabled: true },\n      { value: 'classical', label: 'Classical' },\n      { value: 'electronic', label: 'Electronic (Coming Soon)', disabled: true },\n      { value: 'hip-hop', label: 'Hip Hop' },\n    ],\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Controlled = {\n  render: () => {\n    const [value, setValue] = useState('')\n\n    return (\n      <div style={{ width: '400px', display: 'flex', flexDirection: 'column', gap: '16px' }}>\n        <Select\n          label=\"Music Genre\"\n          placeholder=\"Select genre\"\n          options={genres}\n          value={value}\n          onChange={(e) => setValue(e.target.value)}\n        />\n\n        <div\n          style={{\n            padding: '12px',\n            backgroundColor: 'var(--color-bg-card)',\n            borderRadius: '4px',\n            fontSize: 'var(--font-size-s)',\n            color: 'var(--color-text-secondary)',\n          }}>\n          Selected value: <strong>{value || 'None'}</strong>\n        </div>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Select/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          as=\"label\"\n          htmlFor={selectId}\n          className={clsx(s.label, showError && s.error)}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Select/index.ts",
    "content": "export * from './Select'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/SortSelect/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={selectId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Table/Table.module.css",
    "content": ".table {\n  table-layout: fixed;\n  border-collapse: collapse;\n  width: 100%;\n  background: transparent;\n}\n\n.tableHead {\n  border-bottom: 1px solid var(--color-border-base);\n}\n\n.tableHeaderCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-xs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  text-align: left;\n  text-transform: uppercase;\n\n  background: transparent;\n}\n\n.tableHeaderCell:first-child {\n  padding-left: 16px;\n}\n\n.tableHeaderCell:last-child {\n  padding-right: 16px;\n}\n\n.tableBody {\n  background: transparent;\n}\n\n.tableRow {\n  transition: background-color 200ms ease;\n}\n\n.tableBody .tableRow:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tableCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  vertical-align: middle;\n\n  background: transparent;\n}\n\n.tableCell:first-child {\n  padding-left: 16px;\n}\n\n.tableCell:last-child {\n  padding-right: 16px;\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Table/Table.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ReactionButtons } from '../ReactionButtons'\nimport { Typography } from '../Typography'\nimport { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './Table'\nimport s from './Table.module.css'\n\nconst meta = {\n  title: 'Components/Table',\n  component: Table,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Table>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst trackData = [\n  {\n    id: 1,\n    title: 'Play It Safe',\n    artist: 'Julia Wolf',\n    image: 'https://picsum.photos/40/40?random=1',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 2,\n    title: 'Ocean Front Apt.',\n    artist: 'ayokay',\n    image: 'https://picsum.photos/40/40?random=2',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 3,\n    title: 'Free Spirit',\n    artist: 'Khalid',\n    image: 'https://picsum.photos/40/40?random=3',\n    dateAdded: '2 day ago',\n    duration: '3:02',\n  },\n  {\n    id: 4,\n    title: 'Remind You',\n    artist: 'FRENSHIP',\n    image: 'https://picsum.photos/40/40?random=4',\n    dateAdded: '3 day ago',\n    duration: '4:25',\n  },\n]\n\nexport const BasicTable = {\n  render: () => (\n    <div style={{ width: '600px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Name</TableHeaderCell>\n            <TableHeaderCell>Email</TableHeaderCell>\n            <TableHeaderCell>Role</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell>John Doe</TableCell>\n            <TableCell>john@example.com</TableCell>\n            <TableCell>Admin</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Jane Smith</TableCell>\n            <TableCell>jane@example.com</TableCell>\n            <TableCell>User</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Bob Johnson</TableCell>\n            <TableCell>bob@example.com</TableCell>\n            <TableCell>Editor</TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n\nexport const EmptyTable = {\n  render: () => (\n    <div style={{ width: '500px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Column&nbsp;1</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;2</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;3</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell colSpan={3}>\n              <Typography variant=\"body2\" style={{ textAlign: 'center', padding: '40px 20px' }}>\n                No data available\n              </Typography>\n            </TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Table/Table.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport s from './Table.module.css'\n\n/*\n * Table\n */\n\nexport type TableProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'table'>\n\nexport const Table = ({ children, className, ...props }: TableProps) => {\n  return (\n    <table className={clsx(s.table, className)} {...props}>\n      {children}\n    </table>\n  )\n}\n\n/*\n * TableHead\n */\n\nexport type TableHeadProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'thead'>\n\nexport const TableHead = ({ children, className, ...props }: TableHeadProps) => {\n  return (\n    <thead className={clsx(s.tableHead, className)} {...props}>\n      {children}\n    </thead>\n  )\n}\n\n/*\n * TableBody\n */\n\nexport type TableBodyProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tbody'>\n\nexport const TableBody = ({ children, className, ...props }: TableBodyProps) => {\n  return (\n    <tbody className={clsx(s.tableBody, className)} {...props}>\n      {children}\n    </tbody>\n  )\n}\n\n/*\n * TableRow\n */\n\nexport type TableRowProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tr'>\n\nexport const TableRow = ({ children, className, ...props }: TableRowProps) => {\n  return (\n    <tr className={clsx(s.tableRow, className)} {...props}>\n      {children}\n    </tr>\n  )\n}\n\n/*\n * TableHeaderCell\n */\n\nexport type TableHeaderCellProps = {\n  children?: ReactNode\n  className?: string\n} & ComponentProps<'th'>\n\nexport const TableHeaderCell = ({ children, className, ...props }: TableHeaderCellProps) => {\n  return (\n    <th className={clsx(s.tableHeaderCell, className)} {...props}>\n      {children}\n    </th>\n  )\n}\n\n/*\n * TableCell\n */\n\nexport type TableCellProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'td'>\n\nexport const TableCell = ({ children, className, ...props }: TableCellProps) => {\n  return (\n    <td className={clsx(s.tableCell, className)} {...props}>\n      {children}\n    </td>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Table/index.ts",
    "content": "export * from './Table'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Tabs/Tabs.module.css",
    "content": ".tabsList {\n  display: flex;\n  width: 100%;\n  border-bottom: 1px solid var(--color-text-secondary);\n}\n\n.tabsTrigger {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  flex: 1 1 0;\n  align-items: center;\n  justify-content: center;\n\n  padding: 12px 16px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.tabsTrigger:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.tabsTrigger:not(.active, :disabled):hover {\n  opacity: 0.7;\n}\n\n.tabsTrigger.active {\n  color: var(--color-accent);\n}\n\n.tabsTrigger.active::after {\n  content: '';\n\n  position: absolute;\n  bottom: -1px;\n  left: 0;\n\n  width: 100%;\n  height: 2px;\n\n  background-color: var(--color-accent);\n}\n\n.tabsTrigger.disabled {\n  cursor: default;\n  color: var(--color-disabled);\n}\n\n.tabsContent {\n  padding: 32px 0;\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Tabs/Tabs.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs'\n\nconst meta = {\n  title: 'Components/Tabs',\n  component: Tabs,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Tabs>\n\nexport default meta\n\nexport const BasicTabs = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Tabs defaultValue=\"account\">\n        <TabsList>\n          <TabsTrigger value=\"account\">Account</TabsTrigger>\n          <TabsTrigger value=\"password\">Password</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"account\">\n          <Typography variant=\"body1\">Make changes to your account here.</Typography>\n        </TabsContent>\n        <TabsContent value=\"password\">\n          <Typography variant=\"body1\">Change your password here.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n\nexport const ControlledTabs = {\n  render: () => {\n    const [activeTab, setActiveTab] = useState('tab1')\n\n    return (\n      <div style={{ width: '500px' }}>\n        <Tabs value={activeTab} onValueChange={setActiveTab}>\n          <TabsList>\n            <TabsTrigger value=\"tab1\">Tab 1</TabsTrigger>\n            <TabsTrigger value=\"tab2\">Tab 2</TabsTrigger>\n            <TabsTrigger value=\"tab3\">Tab 3</TabsTrigger>\n          </TabsList>\n          <TabsContent value=\"tab1\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              First Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the first tab. You can put any React content here.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab2\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Second Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the second tab with different information.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab3\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Third Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              And this is the third tab with its own unique content.\n            </Typography>\n          </TabsContent>\n        </Tabs>\n\n        <Card\n          style={{\n            marginTop: '20px',\n          }}>\n          <Typography variant=\"body2\">\n            Active tab: <strong>{activeTab}</strong>\n          </Typography>\n          <div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab1')}>\n              Go to Tab 1\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab2')}>\n              Go to Tab 2\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab3')}>\n              Go to Tab 3\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const DisabledTab = {\n  render: () => (\n    <div style={{ width: '350px' }}>\n      <Tabs defaultValue=\"available\">\n        <TabsList>\n          <TabsTrigger value=\"available\">Available</TabsTrigger>\n          <TabsTrigger value=\"disabled\" disabled>\n            Disabled\n          </TabsTrigger>\n          <TabsTrigger value=\"another\">Another</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"available\">\n          <Typography variant=\"body1\">This tab is available and active.</Typography>\n        </TabsContent>\n        <TabsContent value=\"disabled\">\n          <Typography variant=\"body1\">This content should not be visible.</Typography>\n        </TabsContent>\n        <TabsContent value=\"another\">\n          <Typography variant=\"body1\">This is another available tab.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Tabs/Tabs.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ComponentProps, createContext, type ReactNode, use, useState } from 'react'\n\nimport s from './Tabs.module.css'\n\ntype TabsContextType = {\n  value?: string\n  onValueChange?: (value: string) => void\n}\n\nconst TabsContext = createContext<TabsContextType | null>(null)\n\nconst useTabsContext = () => {\n  const context = use(TabsContext)\n  if (!context) {\n    throw new Error('Tabs compound components must be used within Tabs component')\n  }\n  return context\n}\n\n/*\n * Tabs\n */\n\nexport type TabsProps = {\n  children: ReactNode\n  defaultValue?: string\n  value?: string\n  onValueChange?: (value: string) => void\n} & ComponentProps<'div'>\n\nexport const Tabs = ({\n  children,\n  defaultValue,\n  value: controlledValue,\n  onValueChange,\n  className,\n  ...props\n}: TabsProps) => {\n  const [internalValue, setInternalValue] = useState(defaultValue)\n\n  const isControlled = controlledValue !== undefined\n  const value = isControlled ? controlledValue : internalValue\n\n  const handleValueChange = (newValue: string) => {\n    if (!isControlled) {\n      setInternalValue(newValue)\n    }\n    onValueChange?.(newValue)\n  }\n\n  return (\n    <div className={className} {...props}>\n      <TabsContext value={{ value, onValueChange: handleValueChange }}>{children}</TabsContext>\n    </div>\n  )\n}\n\n/*\n * TabsList\n */\n\nexport type TabsListProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const TabsList = ({ children, className }: TabsListProps) => {\n  return <div className={clsx(s.tabsList, className)}>{children}</div>\n}\n\n/*\n * TabsTrigger\n */\n\nexport type TabsTriggerProps = {\n  children: ReactNode\n  value: string\n  className?: string\n  disabled?: boolean\n}\n\nexport const TabsTrigger = ({ children, value, className, disabled }: TabsTriggerProps) => {\n  const { value: activeValue, onValueChange } = useTabsContext()\n  const isActive = activeValue === value\n\n  const handleClick = () => {\n    if (!disabled) {\n      onValueChange?.(value)\n    }\n  }\n\n  return (\n    <button\n      className={clsx(s.tabsTrigger, isActive && s.active, disabled && s.disabled, className)}\n      onClick={handleClick}\n      disabled={disabled}\n      type=\"button\">\n      {children}\n    </button>\n  )\n}\n\n/*\n * TabsContent\n */\n\nexport type TabsContentProps = {\n  children: ReactNode\n  value: string\n  className?: string\n}\n\nexport const TabsContent = ({ children, value, className }: TabsContentProps) => {\n  const { value: activeValue } = useTabsContext()\n  const isActive = activeValue === value\n\n  if (!isActive) return null\n\n  return <div className={clsx(s.tabsContent, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Tabs/index.ts",
    "content": "export * from './Tabs'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/TagEditor/TagEditor.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n}\n\n.tagsContainer {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n\n  margin-top: 12px;\n  padding: 8px 0;\n}\n\n.tag {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n\n  padding: 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 16px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.deleteButton:enabled:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.counter {\n  margin-top: 8px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/TagEditor/TagEditor.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { TagEditor } from './TagEditor'\n\nconst meta = {\n  title: 'Components/TagEditor',\n  component: TagEditor,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TagEditor>\n\nexport default meta\n\nexport const Basic = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags\"\n          placeholder=\"Add tag and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Skills (max 5)\"\n          placeholder=\"Add skill and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={5}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [tags, setTags] = useState(['React', 'TypeScript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags (disabled)\"\n          placeholder=\"Cannot add tags\"\n          value={tags}\n          onTagsChange={setTags}\n          disabled={true}\n        />\n      </div>\n    )\n  },\n}\n\nexport const PrefilledTags = {\n  render: () => {\n    const [tags, setTags] = useState([\n      'JavaScript',\n      'TypeScript',\n      'React',\n      'Node.js',\n      'CSS',\n      'HTML',\n    ])\n\n    return (\n      <div style={{ width: '450px' }}>\n        <TagEditor\n          label=\"Programming Languages & Technologies\"\n          placeholder=\"Add more technologies...\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={10}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendTags, setFrontendTags] = useState(['React', 'Vue.js'])\n    const [backendTags, setBackendTags] = useState(['Node.js'])\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Frontend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Frontend\"\n            placeholder=\"Add frontend technology...\"\n            value={frontendTags}\n            onTagsChange={setFrontendTags}\n            maxTags={8}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Backend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Backend\"\n            placeholder=\"Add backend technology...\"\n            value={backendTags}\n            onTagsChange={setBackendTags}\n            maxTags={6}\n          />\n        </div>\n\n        <Card>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Summary:\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '4px' }}>\n            Frontend: {frontendTags.length > 0 ? frontendTags.join(', ') : 'None'}\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block' }}>\n            Backend: {backendTags.length > 0 ? backendTags.join(', ') : 'None'}\n          </Typography>\n        </Card>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/TagEditor/TagEditor.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, KeyboardEvent } from 'react'\nimport { useState } from 'react'\n\nimport { DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport s from './TagEditor.module.css'\n\nexport type TagEditorProps = {\n  label?: string\n  placeholder?: string\n  value: string[]\n  onTagsChange: (tags: string[]) => void\n  maxTags?: number\n  disabled?: boolean\n} & ComponentProps<'div'>\n\nexport const TagEditor = ({\n  label,\n  placeholder = 'Add tag and press Enter',\n  value,\n  onTagsChange,\n  className,\n  maxTags,\n  disabled = false,\n  ...props\n}: TagEditorProps) => {\n  const [inputValue, setInputValue] = useState('')\n\n  const addTag = (tag: string) => {\n    const trimmedTag = tag.trim()\n\n    if (!trimmedTag) return\n    if (value.includes(trimmedTag)) return\n    if (maxTags && value.length >= maxTags) return\n\n    onTagsChange([...value, trimmedTag])\n    setInputValue('')\n  }\n\n  const removeTag = (tagToRemove: string) => {\n    onTagsChange(value.filter((tag) => tag !== tagToRemove))\n  }\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      addTag(inputValue)\n    }\n\n    if (e.key === 'Backspace' && !inputValue && value.length > 0) {\n      removeTag(value[value.length - 1])\n    }\n  }\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <TextField\n        label={label}\n        value={inputValue}\n        onChange={(e) => setInputValue(e.target.value)}\n        onKeyDown={handleKeyDown}\n        placeholder={isMaxTagsReached ? 'Max tags reached' : placeholder}\n        disabled={disabled}\n      />\n\n      {value.length > 0 && (\n        <ul className={s.tagsContainer}>\n          {value.map((tag) => (\n            <li key={tag} className={s.tag}>\n              <Typography variant=\"body2\" className={s.tagText}>\n                {tag}\n              </Typography>\n              <IconButton\n                onClick={() => removeTag(tag)}\n                className={s.deleteButton}\n                disabled={disabled}\n                aria-label={`Remove tag ${tag}`}\n                type=\"button\">\n                <DeleteIcon />\n              </IconButton>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} tags\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/TagEditor/index.ts",
    "content": "export * from './TagEditor'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/TextField/TextField.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.icon {\n  position: absolute;\n  top: 50%;\n  left: 12px;\n  transform: translateY(-50%);\n\n  display: flex;\n\n  color: var(--color-text-secondary);\n}\n\n.input {\n  width: 100%;\n  height: 40px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input.large {\n  height: 56px;\n}\n\n.input:disabled {\n  color: var(--color-disabled);\n}\n\n.input:focus,\n.input:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.input:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input.error {\n  border-color: var(--color-text-error);\n}\n\n.input.withIcon {\n  padding-left: 40px;\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/TextField/TextField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport { TextField } from './TextField'\n\nconst meta = {\n  title: 'Components/TextField',\n  component: TextField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TextField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const Search: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    icon: <SearchIcon width={20} height={20} />,\n    inputSize: 'l',\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/TextField/TextField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './TextField.module.css'\n\nexport type TextFieldSize = 'm' | 'l'\n\nexport type TextFieldProps = {\n  errorMessage?: string\n  label?: ReactNode\n  icon?: ReactNode\n  inputSize?: TextFieldSize\n} & ComponentProps<'input'>\n\nexport const TextField = ({\n  className,\n  errorMessage,\n  id,\n  icon,\n  label,\n  inputSize = 'm',\n  ...props\n}: TextFieldProps) => {\n  const showError = Boolean(errorMessage)\n  const inputId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={inputId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.inputWrapper}>\n        {icon && <span className={s.icon}>{icon}</span>}\n        <input\n          className={clsx(\n            s.input,\n            showError && s.error,\n            icon && s.withIcon,\n            inputSize === 'l' && s.large\n          )}\n          id={inputId}\n          type={'text'}\n          {...props}\n        />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/TextField/index.ts",
    "content": "export * from './TextField'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Textarea/Textarea.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.textarea {\n  resize: none;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.textarea:disabled {\n  color: var(--color-disabled);\n}\n\n.textarea:focus,\n.textarea:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.textarea:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.textarea::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.textarea.error {\n  border-color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Textarea/Textarea.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Textarea } from './Textarea'\n\nconst meta = {\n  title: 'Components/Textarea',\n  component: Textarea,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Textarea>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const WithRows: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    rows: 5,\n  },\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Textarea/Textarea.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Textarea.module.css'\n\nexport type TextareaProps = {\n  errorMessage?: string\n  label?: ReactNode\n} & ComponentProps<'textarea'>\n\nexport const Textarea = ({ className, errorMessage, id, label, ...props }: TextareaProps) => {\n  const showError = Boolean(errorMessage)\n  const textareaId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={textareaId}>\n          {label}\n        </Typography>\n      )}\n\n      <textarea className={clsx(s.textarea, showError && s.error)} id={textareaId} {...props} />\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Textarea/index.ts",
    "content": "export * from './Textarea'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Typography/Typography.module.css",
    "content": ".label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.error {\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.h1 {\n  font-size: var(--font-size-xxxl);\n}\n\n.h2 {\n  margin: 0;\n  font-size: var(--font-size-xl);\n  font-weight: 600;\n  line-height: 1.3;\n}\n\n.h3 {\n  margin: 0;\n  font-size: var(--font-size-xs);\n  font-weight: 600;\n  line-height: 1.7;\n}\n\n.body1 {\n  margin: 0;\n  font-size: var(--font-size-l);\n  font-weight: 400;\n}\n\n.body2 {\n  margin: 0;\n  font-size: var(--font-size-m);\n  font-weight: 400;\n  color: var(--color-text-secondary);\n}\n\n.body3 {\n  margin: 0;\n  font-size: var(--font-size-xxs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* ------------------------------------------------------------ */\n\n.caption {\n  margin: 0;\n  font-size: 0.75rem;\n  font-weight: 400;\n  line-height: 1.66;\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Typography/Typography.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from './Typography'\n\nconst meta = {\n  title: 'Components/Typography',\n  component: Typography,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Typography>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllTypography: Story = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>\n      <Typography variant=\"h1\">h1</Typography>\n      <Typography variant=\"h2\">h2</Typography>\n      <Typography variant=\"h3\">h3</Typography>\n      <Typography variant=\"body1\">body1</Typography>\n      <Typography variant=\"body2\">body2</Typography>\n      <Typography variant=\"caption\">caption</Typography>\n      <Typography variant=\"label\">label</Typography>\n      <Typography variant=\"error\">error</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Typography/Typography.tsx",
    "content": "import clsx from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\nimport React from 'react'\n\nimport styles from './Typography.module.css'\n\nconst VARIANT_DEFAULT_COMPONENT: Record<string, ElementType> = {\n  h1: 'h1',\n  h2: 'h2',\n  h3: 'h3',\n  body1: 'p',\n  body2: 'p',\n  body3: 'p',\n  caption: 'span',\n  label: 'label',\n}\n\ntype TypographyVariant =\n  | 'h1'\n  | 'h2'\n  | 'h3'\n  | 'body1'\n  | 'body2'\n  | 'body3'\n  | 'caption'\n  | 'label'\n  | 'error'\n\ntype Props<T extends ElementType> = {\n  variant?: TypographyVariant\n  as?: T\n  children: React.ReactNode\n} & ComponentProps<T>\n\nexport const Typography = <T extends ElementType = 'span'>({\n  variant = 'body1',\n  as,\n  children,\n  className = '',\n  ...props\n}: Props<T>) => {\n  const Component = as || VARIANT_DEFAULT_COMPONENT[variant] || 'span'\n  const variantClass = styles[variant] || ''\n\n  return (\n    <Component className={clsx(variantClass, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/components/Typography/index.ts",
    "content": "export * from './Typography'\n"
  },
  {
    "path": "apps/reatom/src/shared/components/index.ts",
    "content": "export * from './AudioPlayer'\nexport * from './Autocomplete'\nexport * from './Button'\nexport * from './Card'\nexport * from './Dialog'\nexport * from './DropdownMenu'\nexport * from './Hashtag'\nexport * from './IconButton'\nexport * from './ImageUploader'\nexport * from './Pagination'\nexport * from './Progress'\nexport * from './ReactionButtons'\nexport * from './SearchField'\nexport * from './Select'\nexport * from './Table'\nexport * from './Tabs'\nexport * from './TagEditor'\nexport * from './Textarea'\nexport * from './TextField'\nexport * from './Typography'\n"
  },
  {
    "path": "apps/reatom/src/shared/config/config.ts",
    "content": "export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL\nexport const API_KEY = import.meta.env.VITE_API_KEY\nexport const CURRENT_APP_DOMAIN = import.meta.env.BASE_URL\n"
  },
  {
    "path": "apps/reatom/src/shared/hooks/index.ts",
    "content": "export * from './useDebounceValue'\nexport * from './useGetId'\n"
  },
  {
    "path": "apps/reatom/src/shared/hooks/useDebounceValue.ts",
    "content": "import { useEffect, useState } from 'react'\n\nexport const useDebounceValue = <T>(value: T, delay: number = 700): [T] => {\n  const [debounced, setDebounced] = useState(value)\n\n  useEffect(() => {\n    const handler = setTimeout(() => setDebounced(value), delay)\n    return () => clearTimeout(handler)\n  }, [value, delay])\n\n  return [debounced]\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/hooks/useGetId.ts",
    "content": "import { useId } from 'react'\n\n/*\n * Custom hook to get an ID.\n * If an ID is passed from component props, it returns that ID.\n * Otherwise, it generates and returns a new unique ID.\n *\n * @param {string} [idFromComponentProps] - An optional ID passed from ComponentProps.\n * @returns {string} The ID from component props or a generated unique ID.\n */\nexport const useGetId = (idFromComponentProps?: string) => {\n  const generatedId = useId()\n\n  return idFromComponentProps || generatedId\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/AddToPlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const AddToPlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <path\n      fill=\"#000\"\n      d=\"M8.134 4.795v2.456h2.34v.776h-2.34V10.5h-.84V8.026H4.966v-.776h2.328V4.795h.84Z\"\n    />\n    <path\n      fill=\"#fff\"\n      d=\"M5.89 16.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.665 2.333 2.333 0 0 0 0-4.665ZM17.89 14.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.666 2.333 2.333 0 0 0 0-4.666ZM10.902 5.9l10.489-1.998v1l-10.5 2 .011-1.003Z\"\n    />\n    <path fill=\"#fff\" d=\"M8.39 11.5h1v8l-1-.533V11.5ZM20.39 4.964l1-.464v13l-1-.928V4.963Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/ArrowDownIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ArrowDownIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={20}\n    height={20}\n    viewBox=\"0 0 20 20\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M6.175 7.158 10 10.975l3.825-3.817L15 8.333l-5 5-5-5 1.175-1.175Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/ClockIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ClockIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    viewBox=\"0 0 28 28\"\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14 3c6.075 0 11 4.925 11 11s-4.925 11-11 11S3 20.075 3 14 7.925 3 14 3Zm0 2a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm.5 8.5H18v2h-5.5v-7h2v5Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M0 0h28v28H0z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/CreateIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const CreateIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M16 2.667C8.64 2.667 2.667 8.64 2.667 16S8.64 29.333 16 29.333 29.333 23.36 29.333 16 23.36 2.666 16 2.666Zm6.667 14.666h-5.334v5.334h-2.666v-5.334H9.333v-2.666h5.334V9.332h2.666v5.333h5.334v2.667Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/DeleteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DeleteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={10}\n    height={12}\n    viewBox=\"0 0 10 12\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M7.333 4.25v5.833H2.666V4.25h4.667ZM6.458.75H3.54l-.583.583H.916V2.5h8.167V1.333H7.04L6.458.75Zm2.041 2.333h-7v7a1.17 1.17 0 0 0 1.167 1.167h4.667a1.17 1.17 0 0 0 1.166-1.167v-7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/DislikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DislikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 28 28\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-1.12 0-2.217.292-3.185.805L14 10.5h3.5L14 22.167l1.167-10.5h-3.5l1.796-6.289C12.215 4.212 10.512 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.818 4.854 8.376 11.667 14.583 6.382-5.763 11.667-9.637 11.667-14.583 0-3.594-2.824-6.417-6.417-6.417Zm-7.303 16.018c-4.422-3.955-7.28-6.685-7.28-9.601A4.044 4.044 0 0 1 8.75 5.833c.688 0 1.388.175 2.018.49L8.575 14h3.99l-.618 5.518Zm5.705-1.4 2.986-9.951h-3.395l.712-2.124c.42-.14.863-.21 1.295-.21a4.044 4.044 0 0 1 4.083 4.084c0 2.578-2.356 5.168-5.681 8.201Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/DownloadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DownloadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={22}\n    height={22}\n    viewBox=\"0 0 22 22\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M11.733 14.164V5.867h-1.466v8.286l-2.822-3.28-1.112.954 4.668 5.43 4.687-5.427-1.112-.958-2.843 3.292ZM11 0C4.925 0 0 4.925 0 11s4.925 11 11 11 11-4.925 11-11S17.075 0 11 0Zm0 20.533c-5.257 0-9.533-4.277-9.533-9.533 0-5.257 4.276-9.533 9.533-9.533 5.256 0 9.533 4.276 9.533 9.533 0 5.256-4.277 9.533-9.533 9.533Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/EditIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const EditIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m13.888 9.517.844.766-8.305 7.55h-.844v-.766l8.305-7.55Zm3.3-5.017a.966.966 0 0 0-.641.242l-1.678 1.525 3.438 3.125 1.677-1.525a.778.778 0 0 0 0-1.175l-2.145-1.95a.949.949 0 0 0-.65-.242Zm-3.3 2.658L3.75 16.375V19.5h3.438l10.138-9.217-3.438-3.125Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/HomeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const HomeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      d=\"M16.0001 7.58667L22.6667 13.5867V24H20.0001V16H12.0001V24H9.33341V13.5867L16.0001 7.58667ZM16.0001 4L2.66675 16H6.66675V26.6667H14.6667V18.6667H17.3334V26.6667H25.3334V16H29.3334L16.0001 4Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/ImageUploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ImageUploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={35}\n    height={34}\n    fill=\"none\"\n    viewBox=\"0 0 35 34\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M30.834 3.667v20h-20v-20h20Zm0-3.334h-20a3.343 3.343 0 0 0-3.333 3.334v20C7.5 25.5 9 27 10.834 27h20c1.833 0 3.333-1.5 3.333-3.333v-20c0-1.834-1.5-3.334-3.333-3.334ZM16.667 16.45l2.817 3.767 4.133-5.167 5.55 6.95H12.501l4.166-5.55ZM.834 7v23.333c0 1.834 1.5 3.334 3.333 3.334h23.334v-3.334H4.167V7H.834Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/KeyboardArrowLeftIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowLeftIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path fill=\"currentColor\" d=\"M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/KeyboardArrowRightIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowRightIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width={24} height={24} fill=\"none\" {...props}>\n    <path fill=\"#fff\" d=\"M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/LibraryIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LibraryIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"  currentColor\"\n      d=\"M26.667 2.667h-16A2.674 2.674 0 0 0 8 5.332v16C8 22.8 9.2 24 10.667 24h16c1.466 0 2.666-1.2 2.666-2.667v-16c0-1.467-1.2-2.667-2.666-2.667Zm0 16.666a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12Zm-10 .667A3.335 3.335 0 0 0 20 16.666v-5.333a2 2 0 0 1 2-2h.667a1.333 1.333 0 1 0 0-2.667h-2a2 2 0 0 0-2 2v3.196c0 .882-1.119 1.471-2 1.471a3.334 3.334 0 0 0 0 6.667ZM5.333 9.333a1.333 1.333 0 1 0-2.666 0v17.333c0 1.467 1.2 2.667 2.666 2.667h17.334a1.333 1.333 0 0 0 0-2.666H7.333a2 2 0 0 1-2-2V9.332Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/LikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    viewBox=\"0 0 28 28\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-2.03 0-3.978.945-5.25 2.438C12.728 4.445 10.78 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.41 3.967 8.003 9.975 13.463L14 24.908l1.692-1.54c6.008-5.448 9.975-9.041 9.975-13.451 0-3.594-2.824-6.417-6.417-6.417Zm-5.133 18.142-.117.116-.117-.116C8.33 16.613 4.667 13.288 4.667 9.917c0-2.334 1.75-4.084 4.083-4.084 1.797 0 3.547 1.155 4.165 2.754h2.182c.606-1.599 2.356-2.754 4.153-2.754 2.333 0 4.083 1.75 4.083 4.084 0 3.371-3.663 6.696-9.216 11.725Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/LikeIconFill.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIconFill = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 29 28\"\n    width={29}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14.4 6.04a6.137 6.137 0 0 1 8.655.248c2.375 2.47 2.457 6.402.247 8.967L14.4 24.5l-8.902-9.245c-2.21-2.566-2.126-6.504.248-8.967C8.123 3.823 11.927 3.74 14.4 6.04Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M.4 0h28v28H.4z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/LikeInSquareIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeInSquareIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <rect width={32} height={32} fill=\"url(#a)\" rx={2} />\n    <path\n      fill=\"#fff\"\n      d=\"M16 10.158c1.645-1.597 4.186-1.544 5.77.173 1.583 1.717 1.638 4.453.165 6.237L16 23l-5.934-6.432c-1.473-1.784-1.418-4.524.165-6.237 1.585-1.715 4.121-1.773 5.77-.173Z\"\n    />\n    <defs>\n      <linearGradient id=\"a\" x1={1} x2={32} y1={1} y2={30.5} gradientUnits=\"userSpaceOnUse\">\n        <stop stopColor=\"#3822EA\" />\n        <stop offset={1} stopColor=\"#C7E9D7\" />\n      </linearGradient>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/LiveWaveIcon/LiveWaveIcon.module.css",
    "content": ".bar {\n  transform-origin: center bottom;\n  animation: wave 1.2s ease-in-out infinite alternate;\n}\n\n@keyframes wave {\n  0% {\n    transform: scaleY(0.4);\n  }\n\n  50% {\n    transform: scaleY(1);\n  }\n\n  100% {\n    transform: scaleY(0.6);\n  }\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/LiveWaveIcon/LiveWaveIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nimport s from './LiveWaveIcon.module.css'\n\nexport const LiveWaveIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <rect\n      x={2}\n      y={8}\n      width={2}\n      height={8}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '0ms' }}\n    />\n    <rect\n      x={6}\n      y={4}\n      width={2}\n      height={16}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '150ms' }}\n    />\n    <rect\n      x={10}\n      y={6}\n      width={2}\n      height={12}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '300ms' }}\n    />\n    <rect\n      x={14}\n      y={2}\n      width={2}\n      height={20}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '450ms' }}\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/LiveWaveIcon/index.ts",
    "content": "export { LiveWaveIcon } from './LiveWaveIcon'\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/LogoutIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const LogoutIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m17 8-1.41 1.41L17.17 11H9v2h8.17l-1.58 1.58L17 16l4-4-4-4ZM5 5h7V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h7v-2H5V5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/MoreIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const MoreIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={16}\n    height={4}\n    viewBox=\"0 0 16 4\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M2 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM8 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM16 2a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/PauseIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PauseIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 40 40\"\n    width={40}\n    height={40}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"#fff\"\n      d=\"M20 0c11.046 0 20 8.954 20 20s-8.954 20-20 20S0 31.046 0 20 8.954 0 20 0Zm-6 11a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Zm9 0a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/PlayIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlayIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={72}\n    height={72}\n    viewBox=\"0 0 72 72\"\n    fill=\"none\"\n    {...props}>\n    <circle cx={36} cy={36} r={36} fill=\"#FF38B6\" />\n    <path\n      fill=\"#000\"\n      d=\"M49.287 36.512c.865-.486.865-1.7 0-2.186l-19.47-10.93c-.864-.485-1.946.122-1.946 1.093v21.86c0 .971 1.082 1.579 1.947 1.093l19.469-10.93Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/PlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M28 4H4a2.675 2.675 0 0 0-2.667 2.667v18.666C1.333 26.8 2.533 28 4 28h24c1.467 0 2.667-1.2 2.667-2.667V6.667C30.667 5.2 29.467 4 28 4Zm0 21.333H4V6.667h24v18.666ZM10.667 20c0-2.213 1.786-4 4-4 .466 0 .92.093 1.333.24V8h6.667v2.667h-4v9.373a4.003 4.003 0 0 1-4 3.96c-2.214 0-4-1.787-4-4Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/PlusIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlusIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <path\n      fill=\"var(--color-text-secondary)\"\n      d=\"M30 0a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h28ZM15 9v6H9v2h6v6h2v-6h6v-2h-6V9h-2Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/ProfileIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const ProfileIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2Zm0 16h-4.83L12 20.17 9.83 18H5V4h14v14Zm-7-7c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3Zm0-4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1Zm6 8.58c0-2.5-3.97-3.58-6-3.58s-6 1.08-6 3.58V17h12v-1.42ZM8.48 15c.74-.51 2.23-1 3.52-1s2.78.49 3.52 1H8.48Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/RepeatIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const RepeatIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M9.333 9.333h13.334v4L28 8l-5.333-5.333v4h-16v8h2.666V9.332Zm13.334 13.333H9.333v-4L4 24l5.333 5.333v-4h16v-8h-2.666v5.334Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/SearchIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SearchIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 32 32\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m23.775 22.356 5.817 6.137c.56.59.541 1.534-.04 2.1a1.414 1.414 0 0 1-2.024-.04l-5.822-6.145c-1.979 1.522-4.21 2.36-6.695 2.512a11.872 11.872 0 0 1-4.822-.691c-1.556-.563-2.912-1.366-4.07-2.41-1.159-1.042-2.107-2.313-2.843-3.813a12.37 12.37 0 0 1-1.254-4.779 12.41 12.41 0 0 1 .687-4.898c.557-1.58 1.35-2.958 2.378-4.136 1.028-1.177 2.281-2.14 3.76-2.89a11.915 11.915 0 0 1 4.707-1.28c1.66-.102 3.268.129 4.823.692 1.555.563 2.912 1.366 4.07 2.409 1.159 1.043 2.106 2.314 2.843 3.814a12.368 12.368 0 0 1 1.253 4.779 12.567 12.567 0 0 1-.21 3.162 12.259 12.259 0 0 1-.958 2.929 12.892 12.892 0 0 1-1.6 2.548Zm-8.935 1.635a9.024 9.024 0 0 0 3.596-.982 9.525 9.525 0 0 0 2.869-2.216c.786-.9 1.394-1.952 1.823-3.156a9.4 9.4 0 0 0 .53-3.743 9.367 9.367 0 0 0-.963-3.65c-.566-1.143-1.292-2.113-2.178-2.91a9.443 9.443 0 0 0-3.106-1.847 8.992 8.992 0 0 0-3.685-.534 9.025 9.025 0 0 0-3.596.982A9.524 9.524 0 0 0 7.26 8.15c-.785.9-1.393 1.953-1.822 3.157a9.4 9.4 0 0 0-.53 3.742 9.367 9.367 0 0 0 .962 3.65c.567 1.144 1.293 2.114 2.179 2.91a9.443 9.443 0 0 0 3.106 1.848 8.994 8.994 0 0 0 3.685.534Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/ShuffleIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ShuffleIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M14.12 12.227 7.213 5.333l-1.88 1.88 6.893 6.894 1.894-1.88Zm5.213-6.894 2.72 2.72-16.72 16.734 1.88 1.88 16.733-16.72 2.72 2.72V5.334h-7.333Zm.44 12.547-1.88 1.88 4.173 4.173-2.733 2.734h7.333v-7.334l-2.72 2.72-4.173-4.173Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/SkipNextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipNextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"m8 24 11.333-8L8 8v16Zm2.667-10.853L14.707 16l-4.04 2.853v-5.706ZM21.333 8H24v16h-2.667V8Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/SkipPreviousIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipPreviousIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M8 8h2.667v16H8V8Zm4.667 8L24 24V8l-11.333 8Zm8.666 2.853L17.293 16l4.04-2.853v5.706Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/TextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M14.17 5 19 9.83V19H5V5h9.17Zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V9.83c0-.53-.21-1.04-.59-1.41l-4.83-4.83c-.37-.38-.88-.59-1.41-.59ZM7 15h10v2H7v-2Zm0-4h10v2H7v-2Zm0-4h7v2H7V7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/TrackIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TrackIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m16 4 .013 14.067a5.329 5.329 0 0 0-2.666-.734A5.335 5.335 0 0 0 8 22.667 5.335 5.335 0 0 0 13.347 28c2.96 0 5.32-2.387 5.32-5.333V9.333H24V4h-8Zm-2.653 21.333a2.674 2.674 0 0 1-2.667-2.666c0-1.467 1.2-2.667 2.667-2.667 1.466 0 2.666 1.2 2.666 2.667 0 1.466-1.2 2.666-2.666 2.666Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/UploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const UploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M24 20v4H8v-4H5.333v4c0 1.467 1.2 2.667 2.667 2.667h16c1.467 0 2.667-1.2 2.667-2.667v-4H24ZM9.333 12l1.88 1.88 3.454-3.44v10.894h2.666V10.44l3.454 3.44 1.88-1.88L16 5.333 9.333 12Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/VolumeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M4 12v8h5.333L16 26.667V5.333L9.333 12H4Zm9.333-.227v8.454l-2.893-2.894H6.667v-2.666h3.773l2.893-2.894ZM22 16a6 6 0 0 0-3.333-5.373V21.36A5.965 5.965 0 0 0 22 16ZM18.667 4.307v2.746C22.52 8.2 25.333 11.773 25.333 16c0 4.227-2.813 7.8-6.666 8.947v2.746C24.013 26.48 28 21.707 28 16S24.013 5.52 18.667 4.307Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/VolumeMuteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeMuteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width={24} height={24} {...props}>\n    <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n    <g fill=\"currentColor\">\n      <path d=\"M16.25 13.42c.15-.45.25-.92.25-1.42A4.5 4.5 0 0 0 14 7.97v3.2l2.25 2.25z\" />\n      <path d=\"M19 12c0 1.21-.31 2.34-.85 3.32l1.46 1.46A8.973 8.973 0 0 0 21 12c0-3.83-2.4-7.11-5.78-8.4-.59-.23-1.22.23-1.22.86v.19c0 .38.25.71.61.85C17.18 6.54 19 9.06 19 12zM2.1 3.51a.996.996 0 0 0 0 1.41L6.17 9H4c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71v-2.76l3.32 3.32c-.23.13-.47.24-.71.35-.37.16-.6.52-.6.91 0 .7.7 1.2 1.35.94.5-.2.99-.45 1.44-.73l2.28 2.28a.996.996 0 1 0 1.41-1.41L3.51 3.51a.996.996 0 0 0-1.41 0zM12 9.17V6.41c0-.89-1.08-1.34-1.71-.71l-.88.89L12 9.17z\" />\n    </g>\n  </svg>\n)\n"
  },
  {
    "path": "apps/reatom/src/shared/icons/index.ts",
    "content": "export * from './AddToPlaylistIcon'\nexport * from './ArrowDownIcon'\nexport * from './ClockIcon'\nexport * from './CreateIcon'\nexport * from './DeleteIcon'\nexport * from './DislikeIcon'\nexport * from './DownloadIcon'\nexport * from './EditIcon'\nexport * from './HomeIcon'\nexport * from './ImageUploadIcon'\nexport * from './KeyboardArrowLeftIcon'\nexport * from './KeyboardArrowRightIcon'\nexport * from './LibraryIcon'\nexport * from './LikeIcon'\nexport * from './LikeIconFill'\nexport * from './LikeInSquareIcon'\nexport * from './LiveWaveIcon'\nexport * from './LogoutIcon'\nexport * from './MoreIcon'\nexport * from './PauseIcon'\nexport * from './PlayIcon'\nexport * from './PlaylistIcon'\nexport * from './PlusIcon'\nexport * from './ProfileIcon'\nexport * from './RepeatIcon'\nexport * from './SearchIcon'\nexport * from './ShuffleIcon'\nexport * from './SkipNextIcon'\nexport * from './SkipPreviousIcon'\nexport * from './TextIcon'\nexport * from './TrackIcon'\nexport * from './UploadIcon'\nexport * from './VolumeIcon'\nexport * from './VolumeMuteIcon'\n"
  },
  {
    "path": "apps/reatom/src/shared/ui/prerender-ready.tsx",
    "content": "import { useIsFetching } from '@tanstack/react-query'\nimport { useEffect, useState } from 'react'\n\nexport function PrerenderReady() {\n  const isFetching = useIsFetching()\n  const [ready, setReady] = useState(false)\n\n  useEffect(() => {\n    // Когда все запросы ушли в 0, ставим флаг\n    if (!isFetching) {\n      setReady(true)\n    }\n  }, [isFetching])\n\n  // Рендерим только один раз, когда ready=true\n  return ready ? <div id=\"renderer_rendered\" style={{ display: 'none' }} /> : null\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/ui/utils/query-error-handler-for-rhf-factory.ts",
    "content": "import type { FieldValues, Path, UseFormSetError } from 'react-hook-form'\nimport { toast } from 'react-toastify'\n\nimport {\n  isJsonApiErrorDocument,\n  type JsonApiErrorDocument,\n  parseJsonApiErrors,\n} from '../../api/utils/json-api-error.ts'\n\nexport const queryErrorHandlerForRHFFactory = <T extends FieldValues>({\n  setError,\n}: {\n  setError?: UseFormSetError<T>\n}) => {\n  return (err: JsonApiErrorDocument) => {\n    // 400 от сервера в JSON:API формате\n    if (isJsonApiErrorDocument(err)) {\n      const { fieldErrors, globalErrors } = parseJsonApiErrors(err)\n\n      // полевые ошибки\n      for (const [field, message] of Object.entries(fieldErrors)) {\n        setError?.(field as Path<T>, { type: 'server', message })\n      }\n\n      // «глобальные» (без pointer)\n      if (globalErrors.length > 0) {\n        setError?.('root.server', {\n          type: 'server',\n          message: globalErrors.join('\\n'),\n        })\n        toast(globalErrors.join('\\n'))\n      }\n\n      return\n    }\n  }\n}\n\nexport const mutationGlobalErrorHandler = (\n  error: Error,\n  _: unknown,\n  __: unknown,\n  mutation: unknown\n) => {\n  // 400 от сервера в JSON:API формате\n  // @ts-expect-error ignore typing\n  const globalFlag = (mutation.meta as MutationMeta)?.globalErrorHandler\n  // если в meta сказали \"off\" — ничего не делаем\n  if (globalFlag === 'off') {\n    return\n  }\n\n  if (isJsonApiErrorDocument(error)) {\n    const { globalErrors } = parseJsonApiErrors(error)\n\n    // «глобальные» (без pointer)\n    if (globalErrors.length > 0) {\n      toast(globalErrors.join('\\n'))\n    }\n  }\n}\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/index.ts",
    "content": "import * as ValidationUtils from './validators'\n\nexport { ValidationUtils as VU }\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/getType.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getType(object: any) {\n  return Object.prototype.toString.call(object)\n}\n\nexport default getType\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/inNun.ts",
    "content": "function isNaN(value: number) {\n  return Number.isNaN(value)\n}\n\nexport default isNaN\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/index.ts",
    "content": "export { default as getType } from './getType'\nexport { default as inNun } from './inNun'\nexport { default as isArray } from './isArray'\nexport { default as isFunction } from './isFunction'\nexport { default as isNull } from './isNull'\nexport { default as isObject } from './isObject'\nexport { default as isUndefined } from './isUndefined'\nexport { default as isValid } from './isValid'\nexport { default as isValidArray } from './isValidArray'\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/isArray.ts",
    "content": "import getType from './getType'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isArray<T>(value: any): value is T[] {\n  return getType(value) === '[object Array]'\n}\n\nexport default isArray\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/isFunction.ts",
    "content": "import getType from './getType'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isFunction<TFunction extends (...args: any[]) => any>(value: any): value is TFunction {\n  return (\n    getType(value) === '[object AsyncFunction]' ||\n    getType(value) === '[object Function]' ||\n    getType(value) === '[object GeneratorFunction]'\n  )\n}\n\nexport default isFunction\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/isNull.ts",
    "content": "function isNull(value: unknown): value is null {\n  return value === null\n}\n\nexport default isNull\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/isObject.ts",
    "content": "import isArray from './isArray'\nimport isNull from './isNull'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isObject<T extends Record<string, any>>(value: any): value is T {\n  return typeof value === 'object' && !isArray(value) && !isNull(value)\n}\n\nexport default isObject\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/isUndefined.ts",
    "content": "import getType from './getType'\n\nfunction isUndefined(value: unknown): value is undefined {\n  return getType(value) === '[object Undefined]'\n}\n\nexport default isUndefined\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/isValid.ts",
    "content": "import isNull from './isNull'\nimport isUndefined from './isUndefined'\n\nfunction isValid<T>(value: T | null | undefined): value is T {\n  return !isNull(value) && !isUndefined(value)\n}\n\nexport default isValid\n"
  },
  {
    "path": "apps/reatom/src/shared/utils/validators/isValidArray.ts",
    "content": "import isArray from './isArray'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isValidArray<T>(value: any): value is T[]\nfunction isValidArray<T>(value: T[]): value is T[]\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isValidArray(value: any) {\n  return isArray(value) && value.length > 0\n}\n\nexport default isValidArray\n"
  },
  {
    "path": "apps/reatom/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/reatom/src/widgets/Player/Player.module.css",
    "content": ".player {\n  grid-area: player;\n}\n"
  },
  {
    "path": "apps/reatom/src/widgets/Player/Player.tsx",
    "content": "import { useState } from 'react'\n\nimport { AudioPlayer } from '@/shared/components'\n\nimport s from './Player.module.css'\n\nconst MOCK_TRACK = {\n  src: 'https://cdn.uppbeat.io/audio-files/c636d7c86452449b1203fc0bded83e29/4358717fc9da477a52fb18a6cbd3afcc/d154b5ce5ff1a05ae8115a3c678062e8/STREAMING-dreamland-matrika-main-version-31140-02-25.mp3',\n  cover: 'https://unsplash.it/112/112',\n  title: 'Play It Safe',\n  artist: 'Julia Wolf',\n}\n\nexport const Player = () => {\n  const [isPlaying, setIsPlaying] = useState(false)\n  const [isShuffle, setIsShuffle] = useState(false)\n  const [isRepeat, setIsRepeat] = useState(false)\n\n  return (\n    <AudioPlayer\n      {...MOCK_TRACK}\n      isPlaying={isPlaying}\n      setIsPlaying={setIsPlaying}\n      onNext={() => {}}\n      onPrevious={() => {}}\n      isShuffle={isShuffle}\n      isRepeat={isRepeat}\n      onShuffle={() => setIsShuffle(!isShuffle)}\n      onRepeat={() => setIsRepeat(!isRepeat)}\n      className={s.player}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/reatom/src/widgets/Player/index.ts",
    "content": "export * from './Player'\n"
  },
  {
    "path": "apps/reatom/stylelint.config.js",
    "content": "export default {\n  extends: ['stylelint-config-standard', 'stylelint-config-clean-order'],\n  rules: {\n    // Class selector pattern (allow camelCase for CSS modules)\n    'selector-class-pattern': null,\n\n    // Allow unknown at-rules (for CSS modules :global, :local etc)\n    'at-rule-no-unknown': [\n      true,\n      {\n        ignoreAtRules: ['global', 'local'],\n      },\n    ],\n  },\n\n  // File patterns to lint\n  ignoreFiles: ['dist/**/*', 'build/**/*', 'node_modules/**/*'],\n}\n"
  },
  {
    "path": "apps/reatom/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "apps/reatom/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\", // or \"NodeNext\"\n    \"moduleResolution\": \"Bundler\", // or \"NodeNext\",\n    \"noUncheckedIndexedAccess\": true\n  },\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "apps/reatom/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "apps/reatom/vite.config.ts",
    "content": "import path from 'node:path'\n\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  base: '/reatom',\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, 'src'),\n    },\n  },\n  server: {\n    host: true, // ← or '0.0.0.0'\n    port: 5174,\n    strictPort: true,\n    allowedHosts: [\n      'domain.prod', // <-- your custom host\n      'localhost', // (optional) keep localhost too\n    ],\n  },\n})\n"
  },
  {
    "path": "apps/rtk-query/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.cursor\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n*storybook.log\nstorybook-static\n"
  },
  {
    "path": "apps/rtk-query/.storybook/main.ts",
    "content": "import type { StorybookConfig } from '@storybook/react-vite'\n\nconst config: StorybookConfig = {\n  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],\n  addons: [],\n  framework: {\n    name: '@storybook/react-vite',\n    options: {},\n  },\n}\nexport default config\n"
  },
  {
    "path": "apps/rtk-query/.storybook/preview.tsx",
    "content": "import '../src/styles/fonts.css'\nimport '../src/styles/variables.css'\nimport '../src/styles/reset.css'\nimport '../src/styles/global.css'\n\nimport { configureStore } from '@reduxjs/toolkit'\nimport type { Preview } from '@storybook/react-vite'\nimport React from 'react'\nimport { Provider } from 'react-redux'\nimport { BrowserRouter } from 'react-router'\n\nconst mockStore = configureStore({\n  reducer: {},\n})\n\nconst preview: Preview = {\n  parameters: {\n    controls: {\n      matchers: {\n        color: /(background|color)$/i,\n        date: /Date$/i,\n      },\n    },\n  },\n  decorators: [\n    (Story) => (\n      <Provider store={mockStore}>\n        <BrowserRouter>\n          <Story />\n        </BrowserRouter>\n      </Provider>\n    ),\n  ],\n}\n\nexport default preview\n"
  },
  {
    "path": "apps/rtk-query/CLAUDE.md",
    "content": "# MusicFun - RTK Query Stack\n\n## Stack\n\n| Category        | Technology                    | Version         |\n| --------------- | ----------------------------- | --------------- |\n| Framework       | React                         | 19.1.0          |\n| Routing         | React Router                  | 7.6.2           |\n| Server State    | RTK Query (Redux Toolkit)     | 2.8.2           |\n| Client State    | Redux Toolkit (slices)        | 2.8.2           |\n| Build Tool      | Vite                          | 6.3.5           |\n| Language        | TypeScript                    | ~5.8.3          |\n| UI Components   | @headlessui/react             | 2.2.4           |\n| Forms           | react-hook-form + zod         | 7.58.0 / 4.2.0  |\n| i18n            | i18next + react-i18next       | 25.5.2 / 15.7.3 |\n| Infinite Scroll | react-intersection-observer   | 10.0.0          |\n| Notifications   | react-toastify                | 11.0.5          |\n| Image Crop      | react-easy-crop               | 5.4.2           |\n| Auth Mutex      | async-mutex                   | 0.5.0           |\n| Storybook       | Storybook                     | 9.0.8           |\n| CSS             | CSS Modules                   | -               |\n| Linting         | ESLint + Stylelint + Prettier | -               |\n\n## Config\n\n- **Base URL path**: `/rtkquery`\n- **Dev server port**: 5176\n- **API Base**: `import.meta.env.VITE_BASE_URL`\n- **API Key**: `import.meta.env.VITE_API_KEY`\n- **Auth Token fallback**: `import.meta.env.VITE_AUTH_TOKEN`\n\n## Architecture\n\n**Pattern**: Feature-Sliced Design (FSD) с послойной организацией.\n\n```\nsrc/\n├── app/                        # Application layer\n│   ├── api/                    # RTK Query base API setup + auth interceptors\n│   ├── routing/                # React Router routes definition\n│   └── store/                  # Redux store configuration\n├── features/                   # Feature modules (FSD \"features\" layer)\n│   ├── auth/                   # Auth: login modal, OAuth, token management\n│   │   ├── api/                # RTK Query endpoints (login, logout, me, refresh)\n│   │   ├── model/              # Redux slice (auth modal state)\n│   │   └── ui/                 # LoginModal, AccountMenu\n│   ├── playlists/              # Playlists feature\n│   │   ├── api/                # RTK Query endpoints + mocks\n│   │   ├── model/              # Redux slice (create/edit modal) + hooks\n│   │   └── ui/                 # PlaylistCard, PlaylistCardSkeleton, PlaylistActions\n│   ├── tracks/                 # Tracks feature\n│   │   ├── api/                # RTK Query endpoints + mocks\n│   │   ├── model/              # Redux slice (create/edit modal) + hooks\n│   │   └── ui/                 # TrackCard, TrackRow, TracksTable, TrackActions\n│   ├── tags/                   # Tags feature\n│   │   ├── api/                # RTK Query endpoints\n│   │   └── ui/                 # TagsList, SearchTags\n│   ├── artists/                # Artists feature\n│   │   └── api/                # RTK Query endpoints\n│   └── profile/                # User profile feature\n│       ├── model/              # Redux slice (avatar, name, edit modal)\n│       └── ui/                 # EditProfileModal\n├── pages/                      # Page-level components\n│   ├── MainPage/\n│   ├── TracksPage/\n│   ├── PlaylistsPage/\n│   ├── TrackPage/\n│   ├── PlaylistPage/\n│   ├── UserPage/\n│   ├── auth/                   # OAuthCallback page\n│   └── common/                 # Shared page components\n│       ├── ui/                 # PageWithHeader, ContentList, SearchTextField, SortSelect, SearchTags\n│       └── hooks/              # usePageSearchParams\n├── layout/                     # App shell\n│   ├── Header/\n│   ├── Sidebar/\n│   └── Layout.tsx\n├── widgets/                    # Complex UI widgets\n│   └── Player/                 # Music player widget UI\n├── player/                     # Player business logic\n│   ├── playerSlice.ts          # Redux slice (playback, queue, modes)\n│   ├── playerHooks.ts          # Custom hooks for player controls\n│   ├── playerMiddleware.ts     # Redux middleware for player side-effects\n│   ├── player.ts               # Audio element (new Audio())\n│   └── utils/                  # Track conversion utils\n└── shared/                     # Shared layer\n    ├── components/             # UI Kit (Button, Card, Skeleton, Tabs, etc.)\n    ├── hooks/                  # useDebounceValue, useHover, etc.\n    ├── types/                  # Common API types\n    ├── utils/                  # getImageByType, etc.\n    └── icons/                  # SVG icon components\n```\n\n## State Management\n\n### Server State (RTK Query)\n\n- `baseApi` с fetch-based base query\n- Tag types: `['Playlist', 'Track', 'Artist', 'Tag', 'User']`\n- Автоматическая инвалидация кэша через теги\n- Optimistic updates для реакций (like/dislike)\n- `onQueryStarted` hooks для ручного обновления кэша\n\n### Client State (Redux Slices)\n\n| Slice            | Назначение                                                    |\n| ---------------- | ------------------------------------------------------------- |\n| `authSlice`      | Состояние модалки авторизации                                 |\n| `tracksSlice`    | Состояние модалки создания/редактирования трека               |\n| `playlistsSlice` | Состояние модалки создания/редактирования плейлиста           |\n| `profileSlice`   | Аватар, имя пользователя, модалка профиля + localStorage sync |\n| `playerSlice`    | Полное состояние плеера (playback, queue, volume, modes)      |\n\n### Auth Flow\n\n1. OAuth login -> accessToken + refreshToken в localStorage\n2. Bearer token в заголовке каждого запроса\n3. При 401 -> автоматический refresh через async-mutex\n4. Logout -> очистка токенов + сброс API state\n\n## Player Architecture\n\n- **State Machine**: `playbackState` = idle | loading | playing | paused | error\n- **Queue**: `queue[]`, `originalQueue[]`, `queueIndex`, `hasNextTrack`, `hasPreviousTrack`\n- **Modes**: `repeatMode` (off/one/all), `shuffleMode`\n- **Persistence**: volume, repeat, shuffle -> localStorage\n- **Audio**: `new Audio()` в `player.ts`\n- **Middleware**: `playerMiddleware` для side-effects\n\n## Routes\n\n| Route                | Page            | Description                     |\n| -------------------- | --------------- | ------------------------------- |\n| `/`                  | MainPage        | Tags, new playlists, new tracks |\n| `/tracks`            | TracksPage      | All tracks with infinite scroll |\n| `/tracks/:id`        | TrackPage       | Single track detail             |\n| `/tracks/:id/lyrics` | TrackLyricsPage | Track lyrics                    |\n| `/playlists`         | PlaylistsPage   | All playlists with pagination   |\n| `/playlists/:id`     | PlaylistPage    | Playlist detail with tracks     |\n| `/profile/:userId`   | UserPage        | User profile with tabs          |\n| `/oauth/callback`    | OAuthCallback   | OAuth redirect handler          |\n\n## Commands\n\n```bash\npnpm dev          # Start dev server (port 5176)\npnpm build        # Build for production\npnpm storybook    # Start Storybook\n```\n"
  },
  {
    "path": "apps/rtk-query/README.md",
    "content": "TODO:\n[] Add common components for TrackOverview and PlaylistOverview\n[] Refactor autocomplete (Headless UI)\n[] Errors handling\n[] Infinity scroll for tracks\n"
  },
  {
    "path": "apps/rtk-query/eslint.config.js",
    "content": "// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format\nimport js from '@eslint/js'\nimport prettier from 'eslint-config-prettier'\nimport eslintPluginPrettier from 'eslint-plugin-prettier'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport simpleImportSort from 'eslint-plugin-simple-import-sort'\nimport storybook from 'eslint-plugin-storybook'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config(\n  { ignores: ['dist'] },\n  {\n    extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],\n    files: ['**/*.{ts,tsx}'],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n    plugins: {\n      'react-hooks': reactHooks,\n      'react-refresh': reactRefresh,\n      prettier: eslintPluginPrettier,\n      'simple-import-sort': simpleImportSort,\n    },\n    rules: {\n      ...reactHooks.configs.recommended.rules,\n      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],\n      'prettier/prettier': 'warn',\n      'simple-import-sort/imports': 'error',\n      'simple-import-sort/exports': 'error',\n    },\n  },\n  storybook.configs['flat/recommended']\n)\n"
  },
  {
    "path": "apps/rtk-query/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Musicfun</title>\n    <!-- SPA redirect handler for GitHub Pages -->\n    <script>\n      ;(function () {\n        var redirect = sessionStorage.redirect\n        delete sessionStorage.redirect\n        if (redirect && redirect !== location.href) {\n          history.replaceState(null, null, redirect)\n        }\n\n        // Check for spa_redirect query parameter\n        var searchParams = new URLSearchParams(window.location.search)\n        var spaRedirect = searchParams.get('spa_redirect')\n        if (spaRedirect) {\n          searchParams.delete('spa_redirect')\n          var newSearch = searchParams.toString()\n          var newUrl = decodeURIComponent(spaRedirect)\n          sessionStorage.redirect = newUrl\n          window.location.replace(window.location.pathname + (newSearch ? '?' + newSearch : ''))\n        }\n      })()\n    </script>\n  </head>\n  <body>\n    <div id=\"root\">\n      <div id=\"root\">\n        <svg\n          width=\"40\"\n          height=\"40\"\n          viewBox=\"0 0 40 40\"\n          style=\"position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)\">\n          <style>\n            @keyframes bounce {\n              0%,\n              100% {\n                transform: scaleY(0.3);\n              }\n              50% {\n                transform: scaleY(1);\n              }\n            }\n            .bar {\n              animation: bounce 0.8s ease-in-out infinite;\n              transform-origin: bottom;\n            }\n            .bar:nth-child(2) {\n              animation-delay: 0.1s;\n            }\n            .bar:nth-child(3) {\n              animation-delay: 0.2s;\n            }\n            .bar:nth-child(4) {\n              animation-delay: 0.3s;\n            }\n          </style>\n          <rect class=\"bar\" x=\"4\" y=\"10\" width=\"6\" height=\"20\" rx=\"3\" fill=\"#8b5cf6\" />\n          <rect class=\"bar\" x=\"13\" y=\"5\" width=\"6\" height=\"30\" rx=\"3\" fill=\"#a78bfa\" />\n          <rect class=\"bar\" x=\"22\" y=\"8\" width=\"6\" height=\"24\" rx=\"3\" fill=\"#c4b5fd\" />\n          <rect class=\"bar\" x=\"31\" y=\"12\" width=\"6\" height=\"16\" rx=\"3\" fill=\"#ddd6fe\" />\n        </svg>\n      </div>\n    </div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/rtk-query/package.json",
    "content": "{\n  \"name\": \"musicfun-rtk-query\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"type-check\": \"tsc -b\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"lint:css\": \"stylelint \\\"src/**/*.{css,scss}\\\"\",\n    \"lint:css:fix\": \"stylelint \\\"src/**/*.{css,scss}\\\" --fix\",\n    \"format\": \"prettier --write .\",\n    \"preview\": \"vite preview\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"build-storybook\": \"storybook build\"\n  },\n  \"dependencies\": {\n    \"@headlessui/react\": \"^2.2.4\",\n    \"@hookform/resolvers\": \"^5.2.2\",\n    \"@reduxjs/toolkit\": \"2.8.2\",\n    \"async-mutex\": \"0.5.0\",\n    \"i18next\": \"^25.5.2\",\n    \"i18next-browser-languagedetector\": \"^8.2.0\",\n    \"react\": \"19.1.0\",\n    \"react-dom\": \"19.1.0\",\n    \"react-easy-crop\": \"^5.4.2\",\n    \"react-hook-form\": \"7.58.0\",\n    \"react-i18next\": \"^15.7.3\",\n    \"react-intersection-observer\": \"^10.0.0\",\n    \"react-redux\": \"9.2.0\",\n    \"react-router\": \"7.6.2\",\n    \"react-toastify\": \"11.0.5\",\n    \"zod\": \"4.2.0\"\n  },\n  \"devDependencies\": {\n    \"@storybook/react-vite\": \"9.0.8\",\n    \"@types/node\": \"^24.0.1\",\n    \"@types/react\": \"^19.1.2\",\n    \"@types/react-dom\": \"^19.1.2\",\n    \"@vitejs/plugin-react\": \"^4.4.1\",\n    \"clsx\": \"^2.1.1\",\n    \"storybook\": \"9.0.8\",\n    \"stylelint\": \"^16.20.0\",\n    \"stylelint-config-clean-order\": \"^7.0.0\",\n    \"stylelint-config-standard\": \"^38.0.0\",\n    \"typescript\": \"~5.8.3\",\n    \"vite\": \"^6.3.5\"\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/app/App.tsx",
    "content": "import '../shared/translations/i18nConfiguration'\n\nimport { Provider } from 'react-redux'\nimport { BrowserRouter } from 'react-router'\n\nimport { Routing } from './routing'\nimport { store } from './store'\n\nexport const App = () => {\n  return (\n    <BrowserRouter basename=\"/rtkquery\">\n      <Provider store={store}>\n        <Routing />\n      </Provider>\n    </BrowserRouter>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/app/api/base-api.ts",
    "content": "import { createApi } from '@reduxjs/toolkit/query/react'\n\nimport { baseQueryWithReauth } from './base-query-with-refresh-token-flow-api'\n\nexport const baseApi = createApi({\n  reducerPath: 'baseApi',\n  baseQuery: baseQueryWithReauth,\n  tagTypes: ['Playlist', 'Track', 'Artist', 'Tag', 'User'],\n  endpoints: () => ({}),\n})\n"
  },
  {
    "path": "apps/rtk-query/src/app/api/base-query-with-refresh-token-flow-api.ts",
    "content": "import {\n  type BaseQueryFn,\n  type FetchArgs,\n  fetchBaseQuery,\n  type FetchBaseQueryError,\n} from '@reduxjs/toolkit/query/react'\nimport { Mutex } from 'async-mutex'\n\nimport { handleError } from './handleError.ts'\n\nexport const localStorageKeys = {\n  refreshToken: 'rtk-query-musicfun-refresh-token',\n  accessToken: 'rtk-query-musicfun-access-token',\n} as const\n\nconst mutex = new Mutex()\n\n/**Базовый запрос с авторизацией */\nconst baseQuery = fetchBaseQuery({\n  baseUrl: import.meta.env.VITE_BASE_URL,\n  prepareHeaders: (headers) => {\n    if (import.meta.env.VITE_API_KEY) {\n      headers.set('API-KEY', import.meta.env.VITE_API_KEY)\n    }\n    const token =\n      localStorage.getItem(localStorageKeys.accessToken) ?? import.meta.env.VITE_AUTH_TOKEN\n    if (token) {\n      headers.set('Authorization', `Bearer ${token}`)\n    }\n    // headers.set(\"Content-Type\", \"application/json\")\n    // TODO: Мешает этот параметр для отправки файлов, RTK Query сам правильно определяет Content-Type в зависимости от типа данных.\n    return headers\n  },\n  paramsSerializer: (params) => {\n    const searchParams = new URLSearchParams()\n\n    Object.entries(params).forEach(([key, value]) => {\n      if (Array.isArray(value)) {\n        value.forEach((item) => {\n          searchParams.append(key, String(item))\n        })\n      } else if (value !== undefined && value !== null) {\n        searchParams.set(key, String(value))\n      }\n    })\n\n    return searchParams.toString()\n  },\n})\n\n/**Обёртка с логикой рефреша */\nexport const baseQueryWithReauth: BaseQueryFn<\n  string | FetchArgs,\n  unknown,\n  FetchBaseQueryError\n> = async (args, api, extraOptions) => {\n  // Если кто-то уже рефрешит — ждём\n  await mutex.waitForUnlock()\n  // основной запрос\n  let result = await baseQuery(args, api, extraOptions)\n\n  handleError(result)\n\n  if (result.error?.status === 401) {\n    if (!mutex.isLocked()) {\n      const release = await mutex.acquire()\n\n      try {\n        const refreshToken = localStorage.getItem(localStorageKeys.refreshToken)\n        if (!refreshToken) {\n          console.warn('No refresh token available')\n          return result\n        }\n\n        const refreshResult = await baseQuery(\n          {\n            url: 'auth/refresh',\n            method: 'POST',\n            body: { refreshToken },\n          },\n          api,\n          extraOptions\n        )\n\n        if (refreshResult.data) {\n          const newAccessToken = (refreshResult.data as any).accessToken\n          const newRefreshToken = (refreshResult.data as any).refreshToken\n          localStorage.setItem(localStorageKeys.accessToken, newAccessToken)\n          localStorage.setItem(localStorageKeys.refreshToken, newRefreshToken)\n\n          // Повтор запроса с новым токеном\n          result = await baseQuery(args, api, extraOptions)\n        } else {\n          console.log('Logout: refresh token invalid or expired')\n          //api.dispatch(baseApi.endpoints.logout.initiate())\n\n          // можно перенаправить пользователся на страницу login/auth\n          // window.location.href = \"/login\"\n        }\n      } catch (e) {\n        console.error('Token refresh failed:', e)\n      } finally {\n        release()\n      }\n    } else {\n      await mutex.waitForUnlock()\n      result = await baseQuery(args, api, extraOptions)\n    }\n  }\n\n  return result\n}\n"
  },
  {
    "path": "apps/rtk-query/src/app/api/handleError.ts",
    "content": "import {\n  type FetchBaseQueryError,\n  type FetchBaseQueryMeta,\n  type QueryReturnValue,\n} from '@reduxjs/toolkit/query'\n\nimport type { ExtensionsError } from '@/shared/types'\nimport { showErrorToast } from '@/shared/utils'\n\nexport const handleError = (\n  result: QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>\n) => {\n  const error = 'Some error occurred'\n\n  if (result.error) {\n    const extensionsError = (result.error as ExtensionsError)?.data?.extensions\n    const message = (result.error.data as { message: string }).message\n\n    if (result.error.status === 401 || extensionsError?.length) return\n\n    if (result.error.status === 500) {\n      showErrorToast(error)\n    }\n\n    showErrorToast(message)\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/app/api/index.ts",
    "content": "export * from './base-api.ts'\n"
  },
  {
    "path": "apps/rtk-query/src/app/routing/Routing.tsx",
    "content": "import { Route, Routes } from 'react-router'\n\nimport { OAuthCallback } from '@/features/auth'\nimport { Layout } from '@/layout'\nimport { MainPage, PlaylistPage, PlaylistsPage, TrackPage, TracksPage, UserPage } from '@/pages'\nimport { TrackLyricsPage } from '@/pages/TrackLyricsPage'\nimport { Paths } from '@/shared/configs'\n\nexport const Routing = () => (\n  <Routes>\n    <Route path={Paths.Main} element={<Layout />}>\n      <Route index element={<MainPage />} />\n\n      <Route path={Paths.Tracks} element={<TracksPage />} />\n      <Route path={`${Paths.Tracks}/:id`} element={<TrackPage />} />\n      <Route path={`${Paths.TracksLyrics}/:id`} element={<TrackLyricsPage />} />\n\n      <Route path={Paths.Playlists} element={<PlaylistsPage />} />\n      <Route path={`${Paths.Playlists}/:id`} element={<PlaylistPage />} />\n\n      <Route path={`${Paths.Profile}/:userId`} element={<UserPage />} />\n\n      <Route path={Paths.OAuthRedirect} element={<OAuthCallback />} />\n    </Route>\n  </Routes>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/app/routing/index.ts",
    "content": "export * from './Routing'\n"
  },
  {
    "path": "apps/rtk-query/src/app/store/index.ts",
    "content": "export * from './store'\n"
  },
  {
    "path": "apps/rtk-query/src/app/store/store.ts",
    "content": "import { configureStore } from '@reduxjs/toolkit'\n\nimport { authSlice } from '@/features/auth'\nimport { playlistsSlice } from '@/features/playlists'\nimport { profileSlice } from '@/features/profile'\nimport { tracksSlice } from '@/features/tracks'\nimport { playerMiddleware, playerSlice } from '@/player'\n\nimport { baseApi } from '../api'\n\nexport const store = configureStore({\n  reducer: {\n    [baseApi.reducerPath]: baseApi.reducer,\n    [authSlice.name]: authSlice.reducer,\n    [playlistsSlice.name]: playlistsSlice.reducer,\n    [tracksSlice.name]: tracksSlice.reducer,\n    [playerSlice.name]: playerSlice.reducer,\n    [profileSlice.name]: profileSlice.reducer,\n  },\n  middleware: (getDefaultMiddleware) =>\n    getDefaultMiddleware().concat(baseApi.middleware).concat(playerMiddleware),\n})\n\nexport type RootState = ReturnType<typeof store.getState>\nexport type AppDispatch = typeof store.dispatch\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/api/artists-api.ts",
    "content": "import { baseApi } from '@/app/api/base-api.ts'\n\nexport type Artist = {\n  id: string\n  name: string\n}\n\nexport const artistsApi = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    findArtists: build.query<Artist[], string>({\n      query: (name) => `/artists/search?search=${name}`,\n      providesTags: ['Artist'],\n    }),\n    createArtist: build.mutation<Artist, string>({\n      query: (name) => ({\n        url: `/artists`,\n        method: 'POST',\n        body: {\n          data: {\n            type: 'artists',\n            attributes: { name },\n          },\n        },\n      }),\n      invalidatesTags: ['Artist'],\n    }),\n    deleteArtist: build.mutation<void, string>({\n      query: (id) => ({\n        url: `/artists/${id}`,\n        method: 'DELETE',\n      }),\n      invalidatesTags: ['Artist'],\n    }),\n  }),\n})\n\nexport const { useFindArtistsQuery, useCreateArtistMutation, useDeleteArtistMutation } = artistsApi\n\nexport const MOCK_ARTISTS = [\n  {\n    id: '1',\n    name: 'Kanye West',\n    image: 'https://unsplash.it/148/148',\n  },\n  {\n    id: '2',\n    name: 'Drake & The Weeknd & Kanye West',\n    image: 'https://unsplash.it/149/149',\n  },\n  {\n    id: '3',\n    name: 'Frank Ocean',\n    image: 'https://unsplash.it/150/150',\n  },\n  {\n    id: '4',\n    name: 'Headlund',\n    image: 'https://unsplash.it/151/151',\n  },\n  {\n    id: '5',\n    name: 'Rihanna',\n    image: 'https://unsplash.it/152/152',\n  },\n  {\n    id: '6',\n    name: 'Lamar',\n    image: 'https://unsplash.it/153/153',\n  },\n  {\n    id: '7',\n    name: 'The Weeknd',\n    image: 'https://unsplash.it/154/154',\n  },\n  {\n    id: '8',\n    name: 'Kendrick Lamar',\n    image: 'https://unsplash.it/155/155',\n  },\n  {\n    id: '9',\n    name: 'J. Cole',\n    image: 'https://unsplash.it/156/156',\n  },\n  {\n    id: '10',\n    name: 'Lil Uzi Vert',\n    image: 'https://unsplash.it/157/157',\n  },\n]\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/api/index.ts",
    "content": "export * from './artists-api'\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/ui/ArtistCard/ArtistCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n\n  width: 148px;\n  height: 180px;\n}\n\n.image {\n  overflow: hidden;\n\n  width: 148px;\n  height: 148px;\n  border-radius: 50%;\n\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-align: center;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/ui/ArtistCard/ArtistCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ArtistCard } from './ArtistCard'\n\nconst meta: Meta<typeof ArtistCard> = {\n  title: 'entities/ArtistCard',\n  component: ArtistCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof ArtistCard>\n\nexport const Default: Story = {\n  args: {\n    image: 'https://unsplash.it/182/182',\n    name: 'Kanye West',\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    image: 'https://unsplash.it/183/183',\n    name: 'Drake & The Weeknd & Kanye West',\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/ui/ArtistCard/ArtistCard.tsx",
    "content": "import { Typography } from '@/shared/components'\n\nimport s from './ArtistCard.module.css'\n\ntype Props = {\n  image: string\n  name: string\n}\n\nexport const ArtistCard = ({ image, name }: Props) => {\n  return (\n    <div className={s.card}>\n      <div className={s.image}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <Typography variant=\"h3\" className={s.title}>\n        {name}\n      </Typography>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/ui/ArtistCard/index.ts",
    "content": "export * from './ArtistCard'\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/ui/ArtistsTagAutocomplete/ArtistsTagAutocomplete.module.css",
    "content": ""
  },
  {
    "path": "apps/rtk-query/src/features/artists/ui/ArtistsTagAutocomplete/ArtistsTagAutocomplete.tsx",
    "content": "import { useState } from 'react'\n\nimport { Autocomplete } from '@/shared/components'\n\nimport { useFindArtistsQuery } from '../../api'\n\nexport const ArtistsTagAutocomplete = ({\n  value,\n  onChange,\n}: {\n  value: string[]\n  onChange: (value: string[]) => void\n}) => {\n  const [searchTerm, setSearchTerm] = useState('')\n  const { data: tags } = useFindArtistsQuery(searchTerm)\n\n  const options =\n    tags?.map((tag) => ({\n      label: tag.name,\n      value: tag.id,\n    })) ?? []\n\n  return (\n    <Autocomplete\n      label=\"Artists\"\n      value={value}\n      searchTerm={searchTerm}\n      setSearchTerm={setSearchTerm}\n      onChange={onChange}\n      maxTags={5}\n      options={options}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/ui/ArtistsTagAutocomplete/index.ts",
    "content": "export * from './ArtistsTagAutocomplete.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/features/artists/ui/index.ts",
    "content": "export * from './ArtistsTagAutocomplete'\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/api/auth-api.ts",
    "content": "import { baseApi } from '@/app/api/base-api.ts'\nimport { localStorageKeys } from '@/app/api/base-query-with-refresh-token-flow-api'\nimport { hydrateProfileFromStorage } from '@/features/profile'\n\nimport type { AuthTokensResponse, GetMeResponse, OAuthLoginArgs } from './auth-api.types'\n\nexport const authApi = baseApi.injectEndpoints({\n  endpoints: (builder) => ({\n    me: builder.query<GetMeResponse, void>({\n      query: () => 'auth/me',\n      providesTags: ['User'],\n      async onQueryStarted(_arg, { dispatch, queryFulfilled }) {\n        try {\n          const { data } = await queryFulfilled\n          dispatch(hydrateProfileFromStorage({ userId: data.userId })) //! FIXME: temporary implementation until backend issue #160 is fixed\n        } catch {}\n      },\n    }),\n\n    login: builder.mutation<AuthTokensResponse, OAuthLoginArgs>({\n      query: (payload) => ({\n        url: 'auth/login',\n        method: 'POST',\n        body: payload,\n      }),\n      invalidatesTags: ['User'],\n      async onQueryStarted(_arg, { queryFulfilled }) {\n        try {\n          const { data } = await queryFulfilled\n          localStorage.setItem(localStorageKeys.refreshToken, data.refreshToken)\n          localStorage.setItem(localStorageKeys.accessToken, data.accessToken)\n        } catch {}\n      },\n    }),\n\n    logout: builder.mutation<void, void>({\n      query: () => ({\n        url: 'auth/logout',\n        method: 'POST',\n        body: {\n          refreshToken: localStorage.getItem(localStorageKeys.refreshToken)!,\n        },\n      }),\n      async onQueryStarted(_arg, { dispatch, queryFulfilled }) {\n        try {\n          await queryFulfilled\n          localStorage.removeItem(localStorageKeys.accessToken)\n          localStorage.removeItem(localStorageKeys.refreshToken)\n          // TODO: clear profile cache until backend supports user data (#160)\n          dispatch(hydrateProfileFromStorage({}))\n          await dispatch(authApi.util.resetApiState())\n        } catch {}\n      },\n      invalidatesTags: ['User'],\n    }),\n  }),\n})\n\nexport const { useMeQuery, useLoginMutation, useLogoutMutation } = authApi\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/api/auth-api.types.ts",
    "content": "export type GetMeResponse = {\n  userId: string\n  login: string\n}\n\nexport type AuthTokensResponse = {\n  refreshToken: string\n  accessToken: string\n}\n\nexport type OAuthLoginArgs = {\n  code: string\n  redirectUri: string\n  accessTokenTTL: string // e.g. \"3m\"\n  rememberMe: boolean\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/api/index.ts",
    "content": "export * from './auth-api'\nexport * from './auth-api.types'\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/index.ts",
    "content": "export * from './api'\nexport * from './model'\nexport * from './ui'\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/model/auth-slice.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\n\nexport const authSlice = createSlice({\n  name: 'app',\n  initialState: {\n    isAuthModalOpen: false,\n  },\n  reducers: (create) => ({\n    setIsAuthModalOpen: create.reducer<{ isAuthModalOpen: boolean }>((state, action) => {\n      state.isAuthModalOpen = action.payload.isAuthModalOpen\n    }),\n  }),\n  selectors: {\n    selectIsAuthModalOpen: (state) => state.isAuthModalOpen,\n  },\n})\n\nexport const { setIsAuthModalOpen } = authSlice.actions\nexport const { selectIsAuthModalOpen } = authSlice.selectors\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/model/index.ts",
    "content": "export * from './auth-slice'\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/ui/LoginModal/LoginModal.module.css",
    "content": ".dialog {\n  width: 376px;\n  padding-bottom: 22px;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n  align-items: center;\n\n  text-align: center;\n}\n\n.icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 60px;\n  height: 60px;\n  border-radius: 50%;\n\n  font-size: 24px;\n\n  background-color: var(--color-accent);\n}\n\n.button {\n  height: 55px;\n}\n\n.secondary {\n  background-color: #555;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/ui/LoginModal/LoginModal.tsx",
    "content": "import clsx from 'clsx'\nimport { useTranslation } from 'react-i18next'\n\nimport { setIsAuthModalOpen } from '@/features/auth'\nimport { useLoginMutation } from '@/features/auth'\nimport { Button } from '@/shared/components/Button'\nimport { Dialog, DialogContent, DialogHeader } from '@/shared/components/Dialog'\nimport { Typography } from '@/shared/components/Typography'\nimport { Paths } from '@/shared/configs'\nimport { useAppDispatch } from '@/shared/hooks'\n\nimport s from './LoginModal.module.css'\n\nexport const LoginModal = () => {\n  const { t } = useTranslation()\n\n  const [mutate] = useLoginMutation()\n\n  const dispatch = useAppDispatch()\n\n  const handleCloseAuthModal = () => {\n    dispatch(setIsAuthModalOpen({ isAuthModalOpen: false }))\n  }\n\n  /**\n   * Handles the OAuth login process via popup window.\n   * Opens the OAuth authorization popup, listens for the authorization code,\n   * and triggers the login mutation when the code is received.\n   */\n  const loginHandler = () => {\n    const redirectUri = `${window.location.origin}/rtkquery${Paths.OAuthRedirect}`\n    const url = `${import.meta.env.VITE_BASE_URL}/auth/oauth-redirect?callbackUrl=${redirectUri}`\n    window.open(url, 'oauthPopup', 'width=500,height=600')\n\n    const receiveMessage = async (event: MessageEvent) => {\n      if (event.origin !== window.location.origin) {\n        return\n      }\n\n      const { code } = event.data\n      if (code) {\n        window.removeEventListener('message', receiveMessage)\n        mutate({\n          code,\n          accessTokenTTL: '3m',\n          redirectUri,\n          rememberMe: true,\n        })\n        handleCloseAuthModal()\n      }\n    }\n\n    window.addEventListener('message', receiveMessage)\n  }\n\n  return (\n    <Dialog open onClose={handleCloseAuthModal} className={s.dialog}>\n      <DialogHeader />\n\n      <DialogContent className={s.content}>\n        <Typography variant=\"h2\">\n          Millions of Songs. <br /> Free on Musicfun.\n        </Typography>\n\n        <div className={s.icon}>😊</div>\n\n        <Button className={clsx(s.button, s.secondary)} fullWidth onClick={handleCloseAuthModal}>\n          {t('auth.button.continue_without_sign_in')}\n        </Button>\n        <Button className={s.button} variant=\"primary\" fullWidth onClick={loginHandler}>\n          {t('auth.button.sign_in_with_apihub')}\n        </Button>\n      </DialogContent>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/ui/LoginModal/index.ts",
    "content": "export { LoginModal } from './LoginModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/ui/OAuthRedirect/OAuthCallback.module.css",
    "content": ".title {\n  margin: 0;\n  font-size: 250px;\n  text-align: center;\n}\n\n.subtitle {\n  margin: 0;\n  font-size: 50px;\n  text-align: center;\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/ui/OAuthRedirect/OAuthCallback.tsx",
    "content": "import { useEffect } from 'react'\n\n/**\n * OAuth callback handler component.\n * Retrieves the authorization code from the URL, sends it to the parent window, and closes the popup.\n * Used to complete OAuth authentication via a popup window.\n */\nexport const OAuthCallback = () => {\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const code = url.searchParams.get('code') // or code/state if flow is different\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, '*') // better to use exact origin instead of *\n    }\n\n    window.close()\n  }, [])\n\n  return <p>Logging you in...</p>\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/ui/OAuthRedirect/index.ts",
    "content": "export * from './OAuthCallback'\n"
  },
  {
    "path": "apps/rtk-query/src/features/auth/ui/index.ts",
    "content": "export * from './LoginModal'\nexport * from './OAuthRedirect'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/api/index.ts",
    "content": "export * from './playlistsApi'\nexport * from './playlistsApi.types'\nexport { MOCK_PLAYLISTS } from '@/features/playlists/api/mocks.ts'\nexport { MOCK_PLAYLIST } from '@/features/playlists/api/mocks.ts'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/api/mocks.ts",
    "content": "import { CurrentUserReaction } from './playlistsApi.types'\n\nexport const MOCK_PLAYLISTS = [\n  {\n    data: {\n      id: '1',\n      type: 'playlists',\n      attributes: {\n        title: 'Chill Vibes',\n        description: {\n          text: 'Relax and unwind with these chill tracks 🌊',\n        },\n        addedAt: '2025-06-01T12:00:00Z',\n        updatedAt: '2025-06-10T15:30:00Z',\n        order: 1,\n        user: {\n          id: 'user-101',\n          name: 'Alice',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 640,\n              height: 640,\n              fileSize: 204800,\n              url: 'https://unsplash.it/183/183',\n            },\n          ],\n        },\n        tags: ['chill', 'lofi', 'relax'],\n        currentUserReaction: CurrentUserReaction.Like,\n        likesCount: 542,\n      },\n    },\n  },\n  {\n    data: {\n      id: '2',\n      type: 'playlists',\n      attributes: {\n        title: 'Workout Pump',\n        description: {\n          text: 'High energy tracks to keep you moving 💪',\n        },\n        addedAt: '2025-05-20T08:00:00Z',\n        updatedAt: '2025-06-05T18:00:00Z',\n        order: 2,\n        user: {\n          id: 'user-202',\n          name: 'Bob',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 800,\n              height: 800,\n              fileSize: 307200,\n              url: 'https://unsplash.it/184/184',\n            },\n          ],\n        },\n        tags: ['fitness', 'pump', 'motivation'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 123,\n      },\n    },\n  },\n  {\n    data: {\n      id: '3',\n      type: 'playlists',\n      attributes: {\n        title: 'Fantasy Soundtrack',\n        description: {\n          text: 'Epic and magical music for your quests 🏹',\n        },\n        addedAt: '2025-04-15T14:30:00Z',\n        updatedAt: '2025-05-01T10:10:00Z',\n        order: 3,\n        user: {\n          id: 'user-303',\n          name: 'Elrond',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 1024,\n              height: 768,\n              fileSize: 512000,\n              url: 'https://unsplash.it/185/185',\n            },\n          ],\n        },\n        tags: ['fantasy', 'soundtrack', 'epic'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 54,\n      },\n    },\n  },\n  {\n    data: {\n      id: '4',\n      type: 'playlists',\n      attributes: {\n        title: 'Suffer possible assume',\n        description: {\n          text: 'Recently religious responsibility whether only.',\n        },\n        addedAt: '2025-04-29T10:39:13',\n        updatedAt: '2025-06-14T21:01:35',\n        order: 4,\n        user: {\n          id: 'user-4',\n          name: 'Katie',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 936,\n              height: 306,\n              fileSize: 243840,\n              url: 'https://unsplash.it/192/192',\n            },\n          ],\n        },\n        tags: ['any', 'shake', 'white'],\n        currentUserReaction: CurrentUserReaction.Like,\n        likesCount: 3,\n      },\n    },\n  },\n  {\n    data: {\n      id: '5',\n      type: 'playlists',\n      attributes: {\n        title: 'Risk still',\n        description: {\n          text: 'Skin pay sure yeah couple live heart.',\n        },\n        addedAt: '2025-01-26T00:52:16',\n        updatedAt: '2025-06-14T21:00:56',\n        order: 5,\n        user: {\n          id: 'user-5',\n          name: 'Robert',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 525,\n              height: 500,\n              fileSize: 185000,\n              url: 'https://unsplash.it/191/191',\n            },\n          ],\n        },\n        tags: ['term', 'item'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 12,\n      },\n    },\n  },\n  {\n    data: {\n      id: '6',\n      type: 'playlists',\n      attributes: {\n        title: 'Attack through go',\n        description: {\n          text: 'Plan deep sport growth tonight.',\n        },\n        addedAt: '2025-04-07T10:16:19',\n        updatedAt: '2025-06-14T21:02:28',\n        order: 6,\n        user: {\n          id: 'user-6',\n          name: 'Shelly',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 985,\n              height: 44,\n              fileSize: 105000,\n              url: 'https://unsplash.it/190/190',\n            },\n          ],\n        },\n        tags: ['feeling', 'size'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 0,\n      },\n    },\n  },\n  {\n    data: {\n      id: '7',\n      type: 'playlists',\n      attributes: {\n        title: 'Yet woman outside',\n        description: {\n          text: 'Attorney especially child music capital well.',\n        },\n        addedAt: '2025-01-02T16:37:47',\n        updatedAt: '2025-06-14T21:03:26',\n        order: 7,\n        user: {\n          id: 'user-7',\n          name: 'Kristopher',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 541,\n              height: 589,\n              fileSize: 312000,\n              url: 'https://unsplash.it/189/189',\n            },\n          ],\n        },\n        tags: ['week'],\n        currentUserReaction: CurrentUserReaction.Like,\n        likesCount: 12,\n      },\n    },\n  },\n  {\n    data: {\n      id: '8',\n      type: 'playlists',\n      attributes: {\n        title: 'Community',\n        description: {\n          text: 'Visit about occur it fast industry process.',\n        },\n        addedAt: '2025-06-03T22:12:23',\n        updatedAt: '2025-06-14T21:00:31',\n        order: 8,\n        user: {\n          id: 'user-8',\n          name: 'Kimberly',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 376,\n              height: 803,\n              fileSize: 460000,\n              url: 'https://unsplash.it/188/188',\n            },\n          ],\n        },\n        tags: ['serve', 'although', 'item'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 12,\n      },\n    },\n  },\n  {\n    data: {\n      id: '9',\n      type: 'playlists',\n      attributes: {\n        title: 'Dance Lights Forever',\n        description: {\n          text: 'Feel the beat drop and the lights flash 🎉',\n        },\n        addedAt: '2024-12-14T15:20:12',\n        updatedAt: '2025-06-13T17:15:00',\n        order: 9,\n        user: {\n          id: 'user-9',\n          name: 'Jasmine',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 800,\n              height: 800,\n              fileSize: 310000,\n              url: 'https://unsplash.it/187/187',\n            },\n          ],\n        },\n        tags: ['dance', 'party', 'electro'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 2,\n      },\n    },\n  },\n  {\n    data: {\n      id: '10',\n      type: 'playlists',\n      attributes: {\n        title: 'Calm Forest Ambience',\n        description: {\n          text: 'Let nature help you concentrate 🌲',\n        },\n        addedAt: '2025-03-01T09:45:00',\n        updatedAt: '2025-06-10T13:20:00',\n        order: 10,\n        user: {\n          id: 'user-10',\n          name: 'Leo',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 1024,\n              height: 576,\n              fileSize: 280000,\n              url: 'https://unsplash.it/186/186',\n            },\n          ],\n        },\n        tags: ['nature', 'focus', 'relax'],\n        currentUserReaction: CurrentUserReaction.Dislike,\n        likesCount: 84,\n      },\n    },\n  },\n]\nexport const MOCK_PLAYLIST = {\n  data: {\n    id: '10',\n    type: 'playlists',\n    attributes: {\n      title: 'Calm Forest Ambience',\n      description: {\n        text: 'Let nature help you concentrate 🌲',\n      },\n      addedAt: '2025-03-01T09:45:00',\n      updatedAt: '2025-06-10T13:20:00',\n      order: 10,\n      user: {\n        id: 'user-10',\n        name: 'Leo',\n      },\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 1024,\n            height: 576,\n            fileSize: 280000,\n            url: 'https://unsplash.it/300/300',\n          },\n        ],\n      },\n      tags: ['nature', 'focus', 'relax'],\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 12,\n    },\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/api/playlistsApi.ts",
    "content": "import { baseApi } from '@/app/api/base-api.ts'\nimport { CurrentUserReaction } from '@/shared/components'\nimport type { Images, Nullable, ReactionResponse } from '@/shared/types'\n\nimport type {\n  CreatePlaylistArgs,\n  FetchPlaylistsArgs,\n  Playlist,\n  PlaylistDetail,\n  PlaylistsResponse,\n  UpdatePlaylistArgs,\n} from './playlistsApi.types.ts'\n\nexport const playlistsAPI = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    fetchPlaylists: build.query<PlaylistsResponse, FetchPlaylistsArgs>({\n      query: (params) => ({ url: 'playlists', params }),\n      providesTags: ['Playlist', 'Track'],\n    }),\n    fetchPlaylistById: build.query<{ data: PlaylistDetail }, string>({\n      query: (playlistId) => ({ url: `playlists/${playlistId}` }),\n      providesTags: (_result, _error, playlistId) => [{ type: 'Playlist', id: playlistId }],\n    }),\n    createPlaylist: build.mutation<{ data: PlaylistDetail }, CreatePlaylistArgs>({\n      query: ({ title, description }) => ({\n        url: 'playlists',\n        method: 'POST',\n        body: {\n          data: {\n            type: 'playlists',\n            attributes: { title, description },\n          },\n        },\n      }),\n      invalidatesTags: ['Playlist'],\n    }),\n    updatePlaylist: build.mutation<void, { playlistId: string; payload: UpdatePlaylistArgs }>({\n      query: ({ playlistId, payload }) => ({\n        url: `playlists/${playlistId}`,\n        method: 'PUT',\n        body: {\n          data: {\n            type: 'playlists',\n            attributes: payload,\n          },\n        },\n      }),\n      invalidatesTags: (_result, _error, { playlistId }) => [\n        { type: 'Playlist', id: playlistId },\n        'Playlist',\n      ],\n    }),\n    removePlaylist: build.mutation<void, string>({\n      query: (playlistId) => ({\n        url: `playlists/${playlistId}`,\n        method: 'DELETE',\n      }),\n      invalidatesTags: (_result, _error, playlistId) => [\n        { type: 'Playlist', id: playlistId },\n        'Playlist',\n      ],\n    }),\n    uploadPlaylistCover: build.mutation<Images, { playlistId: string; file: File }>({\n      query: ({ playlistId, file }) => {\n        const formData = new FormData()\n        formData.append('file', file)\n        return {\n          url: `playlists/${playlistId}/images/main`,\n          method: 'POST',\n          body: formData,\n        }\n      },\n      invalidatesTags: (_result, _error, { playlistId }) => [\n        { type: 'Playlist', id: playlistId },\n        'Playlist',\n      ],\n    }),\n    reorderPlaylist: build.mutation<void, { playlistId: string; putAfterItemId: Nullable<string> }>(\n      {\n        query: ({ playlistId, putAfterItemId }) => ({\n          url: `playlists/${playlistId}/reorder`,\n          method: 'PUT',\n          body: { putAfterItemId },\n        }),\n        invalidatesTags: (_result, _error, { playlistId }) => [\n          { type: 'Playlist', id: playlistId },\n          'Playlist',\n        ],\n      }\n    ),\n    likePlaylist: build.mutation<ReactionResponse, { id: string }>({\n      query: ({ id }) => ({\n        url: `playlists/${id}/likes`,\n        method: 'POST',\n      }),\n      onQueryStarted: async ({ id }, { dispatch, queryFulfilled, getState }) => {\n        const patchResults: { undo: () => void }[] = []\n\n        const patchCachedQueries = (\n          endpoint: 'fetchPlaylists' | 'fetchPlaylistById',\n          recipe: (state: PlaylistsResponse | { data: Playlist }) => void\n        ) => {\n          const args = playlistsAPI.util.selectCachedArgsForQuery(getState(), endpoint)\n          args.forEach((arg) => {\n            patchResults.push(dispatch(playlistsAPI.util.updateQueryData(endpoint, arg, recipe)))\n          })\n        }\n\n        patchCachedQueries('fetchPlaylists', (state) => {\n          const playlist = (state as PlaylistsResponse).data.find((x) => x.id === id)\n          if (!playlist) return\n          playlist.attributes.likesCount += 1\n          playlist.attributes.currentUserReaction = CurrentUserReaction.Like\n        })\n\n        patchCachedQueries('fetchPlaylistById', (state) => {\n          const playlistAttributes = (state as { data: Playlist }).data.attributes\n          playlistAttributes.likesCount += 1\n          playlistAttributes.currentUserReaction = CurrentUserReaction.Like\n        })\n\n        try {\n          await queryFulfilled\n        } catch {\n          patchResults.forEach((p) => p.undo())\n        }\n      },\n      invalidatesTags: (_result, _error, { id }) => [{ type: 'Playlist', id }, 'Playlist'],\n    }),\n    dislikePlaylist: build.mutation<ReactionResponse, { id: string }>({\n      query: ({ id }) => ({\n        url: `playlists/${id}/dislikes`,\n        method: 'POST',\n      }),\n      onQueryStarted: async ({ id }, { dispatch, queryFulfilled, getState }) => {\n        const patchResults: { undo: () => void }[] = []\n\n        const patchCachedQueries = (\n          endpoint: 'fetchPlaylists' | 'fetchPlaylistById',\n          recipe: (state: PlaylistsResponse | { data: Playlist }) => void\n        ) => {\n          const args = playlistsAPI.util.selectCachedArgsForQuery(getState(), endpoint)\n          args.forEach((arg) => {\n            patchResults.push(dispatch(playlistsAPI.util.updateQueryData(endpoint, arg, recipe)))\n          })\n        }\n\n        patchCachedQueries('fetchPlaylists', (state) => {\n          const playlist = (state as PlaylistsResponse).data.find((x) => x.id === id)\n          if (!playlist) return\n          const playlistAttrs = playlist.attributes\n          if (playlistAttrs.currentUserReaction === CurrentUserReaction.Like) {\n            playlistAttrs.likesCount -= 1\n          }\n          playlistAttrs.dislikesCount += 1\n          playlistAttrs.currentUserReaction = CurrentUserReaction.Dislike\n        })\n\n        patchCachedQueries('fetchPlaylistById', (state) => {\n          const playlistAttributes = (state as { data: Playlist }).data.attributes\n          if (playlistAttributes.currentUserReaction === CurrentUserReaction.Like) {\n            playlistAttributes.likesCount -= 1\n          }\n          playlistAttributes.dislikesCount += 1\n          playlistAttributes.currentUserReaction = CurrentUserReaction.Dislike\n        })\n\n        try {\n          await queryFulfilled\n        } catch {\n          patchResults.forEach((p) => p.undo())\n        }\n      },\n      invalidatesTags: (_result, _error, { id }) => [{ type: 'Playlist', id }, 'Playlist'],\n    }),\n    unReactionPlaylist: build.mutation<ReactionResponse, { id: string }>({\n      query: ({ id }) => ({\n        url: `playlists/${id}/reactions`,\n        method: 'DELETE',\n      }),\n      onQueryStarted: async ({ id }, { dispatch, queryFulfilled, getState }) => {\n        const patchResults: { undo: () => void }[] = []\n\n        const patchCachedQueries = (\n          endpoint: 'fetchPlaylists' | 'fetchPlaylistById',\n          recipe: (state: PlaylistsResponse | { data: Playlist }) => void\n        ) => {\n          const args = playlistsAPI.util.selectCachedArgsForQuery(getState(), endpoint)\n          args.forEach((arg) => {\n            patchResults.push(dispatch(playlistsAPI.util.updateQueryData(endpoint, arg, recipe)))\n          })\n        }\n\n        patchCachedQueries('fetchPlaylists', (state) => {\n          const playlist = (state as PlaylistsResponse).data.find((x) => x.id === id)\n          if (!playlist) return\n          const playlistAttributes = playlist.attributes\n          if (playlistAttributes.currentUserReaction === CurrentUserReaction.Like) {\n            playlistAttributes.likesCount -= 1\n          }\n          playlistAttributes.currentUserReaction = CurrentUserReaction.None\n        })\n\n        patchCachedQueries('fetchPlaylistById', (state) => {\n          const playlistAttributes = (state as { data: Playlist }).data.attributes\n          if (playlistAttributes.currentUserReaction === CurrentUserReaction.Like) {\n            playlistAttributes.likesCount -= 1\n          }\n          playlistAttributes.currentUserReaction = CurrentUserReaction.None\n        })\n\n        // if (byIdArgs.length) {\n        //   byIdArgs.forEach((arg) => {\n        //     patchResults.push(\n        //       dispatch(\n        //         playlistsAPI.util.updateQueryData('fetchPlaylistById', arg, (state) => {\n        //           const playlistAttrs = state.data.attributes\n        //           if (playlistAttrs.currentUserReaction === CurrentUserReaction.Like) {\n        //             playlistAttrs.likesCount -= 1\n        //           }\n        //           playlistAttrs.currentUserReaction = CurrentUserReaction.None\n        //         })\n        //       )\n        //     )\n        //   })\n        // }\n\n        try {\n          await queryFulfilled\n        } catch {\n          patchResults.forEach((p) => p.undo())\n        }\n      },\n      invalidatesTags: (_result, _error, { id }) => [{ type: 'Playlist', id }, 'Playlist'],\n    }),\n  }),\n})\n\nexport const {\n  useFetchPlaylistsQuery,\n  useFetchPlaylistByIdQuery,\n  useCreatePlaylistMutation,\n  useUpdatePlaylistMutation,\n  useRemovePlaylistMutation,\n  useUploadPlaylistCoverMutation,\n  useReorderPlaylistMutation,\n  useLikePlaylistMutation,\n  useDislikePlaylistMutation,\n  useUnReactionPlaylistMutation,\n} = playlistsAPI\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/api/playlistsApi.types.ts",
    "content": "import type { Images, Meta, User } from '@/shared/types/commonApi.types.ts'\n\nexport enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = -1,\n}\n\nexport type Playlist = {\n  id: string\n  type: 'playlists'\n  attributes: PlaylistAttributes\n}\n\nexport type PlaylistDetail = {\n  id: string\n  type: 'playlists'\n  attributes: PlaylistDetailAttributes\n}\n\ntype Tag = {\n  id: string\n  name: string\n}\n\n// Base attributes present in both list and single playlist responses\ntype BasePlaylistAttributes = {\n  title: string\n  addedAt: string\n  updatedAt: string\n  order: number\n  tags: Tag[]\n  images: Images\n  user: User\n  tracksCount: number\n  // likes\n  currentUserReaction: CurrentUserReaction\n  dislikesCount: number\n  likesCount: number\n}\n\n// Attributes for playlist list (description removed from list response)\nexport type PlaylistListAttributes = BasePlaylistAttributes\n\n// Attributes for single playlist (includes description)\nexport type PlaylistDetailAttributes = BasePlaylistAttributes & {\n  description: string\n}\n\n// For backward compatibility - used in list responses\nexport type PlaylistAttributes = PlaylistListAttributes\n\n// Response\nexport type PlaylistsResponse = {\n  data: Playlist[]\n  meta: Meta\n}\n\n// Arguments\nexport type CreatePlaylistArgs = {\n  title: string\n  description: string\n}\n\nexport type UpdatePlaylistArgs = {\n  title?: string\n  description?: string\n  tagIds: string[]\n}\n\nexport type FetchPlaylistsArgs = {\n  pageSize?: number\n  pageNumber?: number\n  search?: string\n  sortBy?: 'addedAt' | 'likesCount'\n  sortDirection?: 'asc' | 'desc'\n  tagsIds?: string[] // e.g.: tagsIds=tag1&tagsIds=tag2\n  userId?: string\n  trackId?: string\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/index.ts",
    "content": "export * from './api'\nexport * from './model'\nexport * from './ui'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/model/hooks/index.ts",
    "content": "export * from './useCreatePlaylistModal'\nexport * from './useEditPlaylistModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/model/hooks/useCreatePlaylistModal.ts",
    "content": "import { useCallback } from 'react'\n\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\n\nimport { openCreateModal, selectIsCreateEditModalOpen } from '../playlists-slice'\n\nexport const useCreatePlaylistModal = () => {\n  const dispatch = useAppDispatch()\n  const isCreatePlaylistModalOpen = useAppSelector(selectIsCreateEditModalOpen)\n\n  const handleOpenCreatePlaylistModal = useCallback(() => {\n    dispatch(openCreateModal())\n  }, [dispatch])\n\n  return {\n    isCreatePlaylistModalOpen,\n    handleOpenCreatePlaylistModal,\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/model/hooks/useEditPlaylistModal.ts",
    "content": "import { useCallback } from 'react'\n\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\n\nimport {\n  openEditModal,\n  selectEditingPlaylistId,\n  selectIsCreateEditModalOpen,\n} from '../playlists-slice'\n\nexport const useEditPlaylistModal = () => {\n  const dispatch = useAppDispatch()\n  const isCreateEditModalOpen = useAppSelector(selectIsCreateEditModalOpen)\n  const editingPlaylistId = useAppSelector(selectEditingPlaylistId)\n\n  const handleOpenEditPlaylistModal = useCallback(\n    (playlistId: string) => {\n      dispatch(openEditModal(playlistId))\n    },\n    [dispatch]\n  )\n\n  return {\n    isEditPlaylistModalOpen: isCreateEditModalOpen && editingPlaylistId,\n    handleOpenEditPlaylistModal,\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/model/index.ts",
    "content": "export * from './hooks'\nexport * from './playlists-slice'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/model/playlists-slice.ts",
    "content": "import { createSlice, type PayloadAction } from '@reduxjs/toolkit'\n\nconst initialState = {\n  createEditModal: {\n    isOpen: false,\n    playlistId: null as string | null,\n  },\n}\n\nexport const playlistsSlice = createSlice({\n  name: 'playlists',\n  initialState,\n  reducers: {\n    openCreateModal: (state) => {\n      state.createEditModal.isOpen = true\n      state.createEditModal.playlistId = null\n    },\n    openEditModal: (state, action: PayloadAction<string>) => {\n      state.createEditModal.isOpen = true\n      state.createEditModal.playlistId = action.payload\n    },\n    closeCreateEditModal: (state) => {\n      state.createEditModal.isOpen = false\n      state.createEditModal.playlistId = null\n    },\n  },\n  selectors: {\n    selectCreateEditModal: (state) => state.createEditModal,\n    selectIsCreateEditModalOpen: (state) => state.createEditModal.isOpen,\n    selectEditingPlaylistId: (state) => state.createEditModal.playlistId,\n  },\n})\n\nexport const { openCreateModal, openEditModal, closeCreateEditModal } = playlistsSlice.actions\nexport const { selectCreateEditModal, selectIsCreateEditModalOpen, selectEditingPlaylistId } =\n  playlistsSlice.selectors\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/ChoosePlaylistButtonAndModal/ChoosePlaylistButtonAndModal.module.css",
    "content": ".selected {\n  opacity: 1;\n  background-color: rgb(255 56 182 / 30%);\n  outline: 1px solid #e052c6;\n  box-shadow: 0 0 0 2px #e052c6;\n}\n\n.chooseButton {\n  all: unset;\n\n  cursor: pointer;\n\n  display: flex;\n\n  box-sizing: border-box;\n  width: 100%;\n  min-height: 40px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.chooseButton:hover,\n.chooseButton:focus {\n  background-color: var(--color-bg-input-hover);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/ChoosePlaylistButtonAndModal/ChoosePlaylistButtonAndModal.tsx",
    "content": "import { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\n\nimport { useMeQuery } from '@/features/auth'\nimport { useFetchPlaylistsQuery } from '@/features/playlists'\nimport { ChoosePlaylistModal } from '@/features/playlists/ui/ChoosePlaylistModal/ChoosePlaylistModal.tsx'\nimport { Typography } from '@/shared/components'\n\nimport s from './ChoosePlaylistButtonAndModal.module.css'\n\nexport const ChoosePlaylistButtonAndModal = ({\n  playlistIds,\n  setPlaylistIds,\n}: {\n  playlistIds: string[]\n  setPlaylistIds: (playlistIds: string[]) => void\n}) => {\n  const { t } = useTranslation()\n\n  const { data: user } = useMeQuery()\n\n  const [isOpen, setIsOpen] = useState(false)\n\n  const { data: playlists } = useFetchPlaylistsQuery({\n    userId: user?.userId,\n  })\n\n  const selectedPlaylists = playlists?.data.filter((p) => playlistIds.includes(p.id)) ?? []\n\n  return (\n    <>\n      <div>\n        <Typography variant=\"label\" as=\"p\">\n          Choose playlist\n        </Typography>\n        <button type=\"button\" className={s.chooseButton} onClick={() => setIsOpen(true)}>\n          {playlistIds.length > 0 ? (\n            <span>{selectedPlaylists.map((p) => p.attributes.title).join(', ')}</span>\n          ) : (\n            t('playlists.button.choose_playlist')\n          )}\n        </button>\n      </div>\n      <ChoosePlaylistModal\n        isOpen={isOpen}\n        setIsOpen={setIsOpen}\n        playlistIds={playlistIds}\n        setPlaylistIds={setPlaylistIds}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/ChoosePlaylistButtonAndModal/index.ts",
    "content": "export { ChoosePlaylistButtonAndModal } from './ChoosePlaylistButtonAndModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/ChoosePlaylistModal/ChoosePlaylistModal.module.css",
    "content": ".playlistList {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 24px;\n\n  margin: 0;\n  padding: 0;\n\n  list-style: none;\n}\n\n.playlistItem {\n  cursor: pointer;\n\n  display: flex;\n\n  padding: 12px;\n  border: 2px solid transparent;\n\n  background: #18181b;\n\n  transition:\n    border 0.2s,\n    opacity 0.2s,\n    background-color 0.2s;\n}\n\n.playlistItem:not(.selected):focus-within,\n.playlistItem:not(.selected):hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.selected {\n  opacity: 1;\n  background-color: rgb(255 56 182 / 30%);\n  outline: 1px solid #e052c6;\n  box-shadow: 0 0 0 2px #e052c6;\n}\n\n.playlistLabel {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n\n  width: 100%;\n\n  outline: none;\n}\n\n.imageWrapper {\n  overflow: hidden;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 120px;\n  height: 120px;\n  margin-bottom: 12px;\n  border-radius: 8px;\n\n  background: #222;\n}\n\n.imageWrapper img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.playlistTitle {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.playlistTracks {\n  font-size: 0.9rem;\n  color: #b3b3b3;\n}\n\n/* hidden checkbox for accessibility */\n.playlistLabel input[type='checkbox'] {\n  pointer-events: none;\n\n  position: absolute;\n\n  width: 1px;\n  height: 1px;\n\n  opacity: 0;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/ChoosePlaylistModal/ChoosePlaylistModal.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { useMeQuery } from '@/features/auth'\nimport { useFetchPlaylistsQuery } from '@/features/playlists'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  Typography,\n} from '@/shared/components'\nimport { ImageType } from '@/shared/types'\nimport { getImageByType } from '@/shared/utils'\n\nimport s from './ChoosePlaylistModal.module.css'\n\nexport const ChoosePlaylistModal = ({\n  playlistIds,\n  isOpen,\n  setIsOpen,\n  setPlaylistIds,\n  onChoose,\n}: {\n  isOpen: boolean\n  setIsOpen: (isOpen: boolean) => void\n  playlistIds: string[]\n  setPlaylistIds: (playlistIds: string[]) => void\n  onChoose?: () => void\n}) => {\n  const { t } = useTranslation()\n\n  const { data: user } = useMeQuery()\n\n  const { data: playlists } = useFetchPlaylistsQuery({\n    userId: user?.userId,\n  })\n\n  function handleToggle(id: string) {\n    if (playlistIds.includes(id)) {\n      setPlaylistIds(playlistIds.filter((pid) => pid !== id))\n    } else {\n      setPlaylistIds([...playlistIds, id])\n    }\n  }\n\n  return (\n    <Dialog\n      open={isOpen}\n      onClose={() => setIsOpen(false)}\n      aria-modal=\"true\"\n      aria-labelledby=\"choose-playlist-title\">\n      <DialogHeader>\n        <Typography variant=\"h2\">Choose playlist</Typography>\n      </DialogHeader>\n      <DialogContent>\n        <ul className={s.playlistList}>\n          {playlists?.data.map((playlist) => {\n            const image = getImageByType(playlist.attributes.images, ImageType.MEDIUM)\n            const checked = playlistIds.includes(playlist.id)\n            return (\n              <li key={playlist.id} className={s.playlistItem + (checked ? ' ' + s.selected : '')}>\n                <label\n                  className={s.playlistLabel}\n                  tabIndex={0}\n                  onKeyDown={(e) => {\n                    if (e.key === ' ' || e.key === 'Enter') {\n                      e.preventDefault()\n                      handleToggle(playlist.id)\n                    }\n                  }}>\n                  <input\n                    type=\"checkbox\"\n                    checked={checked}\n                    onChange={() => handleToggle(playlist.id)}\n                    tabIndex={-1}\n                    aria-checked={checked}\n                    aria-label={`Select playlist ${playlist.attributes.title}`}\n                  />\n                  <div className={s.imageWrapper}>\n                    <img src={image?.url || noCoverPlaceholder} alt={playlist.attributes.title} />\n                  </div>\n                  <Typography variant=\"h3\" className={s.playlistTitle}>\n                    {playlist.attributes.title}\n                  </Typography>\n                </label>\n              </li>\n            )\n          })}\n        </ul>\n      </DialogContent>\n      <DialogFooter>\n        <Button variant=\"secondary\" onClick={() => setIsOpen(false)}>\n          Cancel\n        </Button>\n        <Button\n          variant=\"primary\"\n          onClick={() => {\n            setIsOpen(false)\n            onChoose?.()\n          }}\n          disabled={playlistIds.length === 0}>\n          {t('button.choose')}\n        </Button>\n      </DialogFooter>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/ChoosePlaylistModal/index.ts",
    "content": "export * from './ChoosePlaylistModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/CreateEditPlaylistModal/CreateEditPlaylistModal.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 745px;\n}\n\n.form {\n  overflow-y: auto;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n  margin-bottom: 16px;\n}\n\n.imageUploader {\n  width: 280px;\n  margin: 0 auto;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/CreateEditPlaylistModal/CreateEditPlaylistModal.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { useForm } from 'react-hook-form'\nimport { useTranslation } from 'react-i18next'\n\nimport {\n  useCreatePlaylistMutation,\n  useFetchPlaylistByIdQuery,\n  useUpdatePlaylistMutation,\n  useUploadPlaylistCoverMutation,\n} from '@/features/playlists'\nimport { closeCreateEditModal, selectEditingPlaylistId } from '@/features/playlists'\nimport { PlaylistTagAutocomplete } from '@/features/tags/ui'\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  ImageUploader,\n  Textarea,\n  TextField,\n  Typography,\n} from '@/shared/components'\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\nimport { showErrorToast } from '@/shared/utils'\n\nimport s from './CreateEditPlaylistModal.module.css'\n\ntype FormData = {\n  title: string\n  description: string\n  tags: string[]\n}\n\nexport const CreateEditPlaylistModal = () => {\n  const { t } = useTranslation()\n\n  const dispatch = useAppDispatch()\n  const editingPlaylistId = useAppSelector(selectEditingPlaylistId)\n\n  const [selectedImage, setSelectedImage] = useState<File | null>(null)\n\n  const isEditMode = !!editingPlaylistId\n\n  // Запрос данных плейлиста для редактирования\n  const { data: playlistData } = useFetchPlaylistByIdQuery(editingPlaylistId!, {\n    skip: !editingPlaylistId,\n  })\n\n  const playlistCoverUrl = playlistData?.data.attributes.images.main.find(\n    (image) => image.type === 'original'\n  )?.url\n\n  const [createPlaylist] = useCreatePlaylistMutation()\n  const [updatePlaylist] = useUpdatePlaylistMutation()\n  const [uploadPlaylistCover] = useUploadPlaylistCoverMutation()\n\n  const {\n    register,\n    handleSubmit,\n    setValue,\n    watch,\n    reset,\n    formState: { errors, isSubmitting },\n  } = useForm<FormData>({\n    defaultValues: {\n      title: '',\n      description: '',\n      tags: [],\n    },\n  })\n\n  const tagsValue = watch('tags')\n\n  // Initial values\n  useEffect(() => {\n    if (isEditMode && playlistData?.data) {\n      const playlist = playlistData.data.attributes\n      reset({\n        title: playlist.title,\n        description: playlist.description,\n        tags: playlist.tags.map((tag) => tag.id),\n      })\n    }\n  }, [isEditMode, playlistData, reset])\n\n  const handleClose = () => {\n    dispatch(closeCreateEditModal())\n  }\n\n  const handleTagsChange = (tags: string[]) => {\n    setValue('tags', tags)\n  }\n\n  const handleImageSelect = (file: File) => {\n    setSelectedImage(file)\n  }\n\n  const onSubmit = async (data: FormData) => {\n    try {\n      if (isEditMode) {\n        await updatePlaylist({\n          playlistId: editingPlaylistId,\n          payload: {\n            title: data.title,\n            description: data.description,\n            tagIds: data.tags,\n          },\n        }).unwrap()\n\n        if (selectedImage) {\n          await uploadPlaylistCover({\n            playlistId: editingPlaylistId,\n            file: selectedImage,\n          })\n\n          handleClose()\n        } else {\n          handleClose()\n        }\n      } else {\n        const createResult = await createPlaylist({\n          title: data.title,\n          description: data.description,\n        }).unwrap()\n\n        const playlistId = createResult.data.id\n\n        const updatePromise = updatePlaylist({\n          playlistId: createResult.data.id,\n          payload: {\n            ...createResult.data.attributes,\n            tagIds: data.tags,\n          },\n        }).unwrap()\n\n        const uploadImagePromise = selectedImage\n          ? uploadPlaylistCover({\n              playlistId,\n              file: selectedImage,\n            }).unwrap()\n          : Promise.resolve()\n\n        try {\n          await Promise.all([updatePromise, uploadImagePromise])\n        } catch (error) {\n          console.error('Failed to perform secondary actions (tags/image):', error)\n        }\n\n        handleClose()\n      }\n    } catch (error) {\n      console.error(`Failed to ${isEditMode ? 'update' : 'create'} playlist:`, error)\n      showErrorToast(`Failed to ${isEditMode ? 'update' : 'create'} playlist`)\n    }\n  }\n\n  return (\n    <Dialog open onClose={handleClose} className={s.dialog}>\n      <DialogHeader>\n        <Typography variant=\"h2\">\n          {isEditMode ? t('playlists.title.edit_playlist') : t('playlists.title.create_playlist')}\n        </Typography>\n      </DialogHeader>\n\n      <form onSubmit={handleSubmit(onSubmit)} className={s.form}>\n        <DialogContent className={s.content}>\n          <ImageUploader\n            className={s.imageUploader}\n            onImageSelect={handleImageSelect}\n            enableCrop\n            cropShape=\"rect\"\n            initialImageUrl={isEditMode ? playlistCoverUrl : undefined}\n          />\n\n          <TextField\n            {...register('title', {\n              required: t('title.required'),\n              minLength: {\n                value: 2,\n                message: t('title.min_value', { quantity: '2' }),\n              },\n              maxLength: {\n                value: 100,\n                message: t('title.max_value', { quantity: '100' }),\n              },\n            })}\n            label={t('title.title')}\n            placeholder={t('playlists.placeholder.enter_playlist_title')}\n            errorMessage={errors.title?.message}\n          />\n\n          <Textarea\n            {...register('description', {\n              maxLength: {\n                value: 500,\n                message: t('description.title.max_value', { quantity: '500' }),\n              },\n            })}\n            rows={3}\n            label={t('description.label.description')}\n            placeholder={t('playlists.placeholder.enter_playlist_description')}\n            errorMessage={errors.description?.message}\n          />\n\n          <PlaylistTagAutocomplete value={tagsValue} onChange={handleTagsChange} />\n        </DialogContent>\n\n        <DialogFooter>\n          <Button variant=\"secondary\" onClick={handleClose} type=\"button\" disabled={isSubmitting}>\n            {t('button.cancel')}\n          </Button>\n          <Button variant=\"primary\" type=\"submit\" disabled={isSubmitting}>\n            {isSubmitting\n              ? isEditMode\n                ? 'Updating...'\n                : 'Creating...'\n              : isEditMode\n                ? t('button.update')\n                : t('button.create')}\n          </Button>\n        </DialogFooter>\n      </form>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/CreateEditPlaylistModal/index.ts",
    "content": "export { CreateEditPlaylistModal } from './CreateEditPlaylistModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistActions/PlaylistActions.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { useRemovePlaylistMutation } from '@/features/playlists/api'\nimport { useEditPlaylistModal } from '@/features/playlists/model'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/shared/components'\nimport { DeleteIcon, EditIcon, MoreIcon } from '@/shared/icons'\n\ntype PlaylistActionsProps = {\n  playlistId: string\n}\n\nexport const PlaylistActions = ({ playlistId }: PlaylistActionsProps) => {\n  const { t } = useTranslation()\n  const { handleOpenEditPlaylistModal } = useEditPlaylistModal()\n  const [removePlaylist] = useRemovePlaylistMutation()\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem\n          onClick={() => {\n            handleOpenEditPlaylistModal(playlistId)\n          }}>\n          <EditIcon />\n          {t('button.edit')}\n        </DropdownMenuItem>\n\n        <DropdownMenuItem\n          onClick={() => {\n            removePlaylist(playlistId)\n          }}>\n          <DeleteIcon width={24} height={24} />\n          {t('button.delete')}\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistActions/index.ts",
    "content": "export * from './PlaylistActions'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistCard/PlaylistCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n\n  width: 264px;\n  min-height: 225px;\n}\n\n.card.withReactionButtons {\n  min-height: 267px;\n}\n\n.header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.image {\n  overflow: hidden;\n  height: 153px;\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.8;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.description {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.reactionButtons {\n  margin-top: auto;\n}\n\n.detailsRow {\n  display: flex;\n  gap: 4px;\n  min-width: 0;\n}\n\n.dot::before {\n  content: '•';\n  margin: 0 4px;\n  font-size: 17px;\n  opacity: 0.7;\n}\n\n.madeFor {\n  overflow: hidden;\n  display: flex;\n  gap: 4px;\n  align-items: baseline;\n\n  width: 100%;\n  min-width: 0;\n}\n\n.userLink {\n  cursor: pointer;\n\n  overflow: hidden;\n  display: inline-block;\n  flex-shrink: 1;\n\n  min-width: 0;\n  max-width: 100%;\n\n  line-height: inherit;\n  color: white;\n  text-decoration: underline;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n\n  background: none;\n\n  transition: transform 0.4s;\n}\n\n.userLink:hover {\n  transform: scale(1.02);\n}\n\n.userLink:active {\n  transform: scale(1.005);\n}\n\n.created {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.tracks {\n  flex-shrink: 0;\n}\n\n.madeForText {\n  flex-shrink: 0;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistCard/PlaylistCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { CurrentUserReaction } from '@/features/playlists'\nimport { DropdownMenu, DropdownMenuTrigger } from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport { PlaylistCard } from './PlaylistCard'\n\nconst meta: Meta<typeof PlaylistCard> = {\n  title: 'entities/PlaylistCard',\n  component: PlaylistCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistCard>\n\nexport const Default: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    imageSrc: 'https://unsplash.it/182/182',\n    // description: 'A playlist for relaxing and unwinding.',\n  },\n}\n\nexport const WithReactions: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    imageSrc: 'https://unsplash.it/182/182',\n    // description: 'A playlist for relaxing and unwinding.',\n    isShowReactionButtons: true,\n    reaction: CurrentUserReaction.Like,\n    likesCount: 10,\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    id: '1',\n    title: 'The Best Hits of Elton John',\n    imageSrc: 'https://unsplash.it/183/183',\n    // description:\n    //   'A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding.',\n  },\n}\n\nexport const WithActions: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    imageSrc: 'https://unsplash.it/182/182',\n    // description: 'A playlist for relaxing and unwinding.',\n    actions: (\n      <DropdownMenu>\n        <DropdownMenuTrigger>\n          <MoreIcon />\n        </DropdownMenuTrigger>\n      </DropdownMenu>\n    ),\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistCard/PlaylistCard.tsx",
    "content": "import clsx from 'clsx'\nimport { useTranslation } from 'react-i18next'\nimport { Link } from 'react-router'\n\nimport {\n  useDislikePlaylistMutation,\n  useLikePlaylistMutation,\n  useUnReactionPlaylistMutation,\n} from '@/features/playlists'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { Card, CurrentUserReaction, ReactionButtons, Typography } from '@/shared/components'\nimport { Paths } from '@/shared/configs'\nimport { formatCreatedDate } from '@/shared/utils/format-created-date.ts'\n\nimport s from './PlaylistCard.module.css'\n\ntype PlaylistCardPropsBase = {\n  id: string\n  title: string\n  imageSrc?: string\n  actions?: React.ReactNode\n  userName?: string\n  userId?: string\n  addedAt?: string\n  tracksCount?: number\n  shouldShowOwnerName?: boolean\n  shouldShowCreatedDate?: boolean\n}\n\ntype PlaylistCardPropsWithReactions = PlaylistCardPropsBase & {\n  isShowReactionButtons: true\n  reaction: CurrentUserReaction\n  likesCount: number\n}\n\ntype PlaylistCardPropsWithoutReactions = PlaylistCardPropsBase & {\n  isShowReactionButtons?: false\n}\n\ntype PlaylistCardProps = PlaylistCardPropsWithReactions | PlaylistCardPropsWithoutReactions\n\nexport const PlaylistCard = ({\n  title,\n  imageSrc = noCoverPlaceholder,\n  id,\n  isShowReactionButtons,\n  actions,\n  userName,\n  userId,\n  addedAt,\n  tracksCount,\n  shouldShowOwnerName = false,\n  shouldShowCreatedDate = false,\n  ...props\n}: PlaylistCardProps) => {\n  const [like] = useLikePlaylistMutation()\n  const [dislike] = useDislikePlaylistMutation()\n  const [unReaction] = useUnReactionPlaylistMutation()\n  const { t } = useTranslation()\n\n  const handleUserNameClick = (e: React.MouseEvent) => {\n    e.stopPropagation()\n  }\n\n  return (\n    <Card className={clsx(s.card, isShowReactionButtons && s.withReactionButtons)}>\n      <Link\n        to={`${Paths.Playlists}/${id}`}\n        className={s.imageLink}\n        aria-label={t('playlists.aria_labels.open_playlist', { title })}>\n        <div className={s.image}>\n          <img src={imageSrc} alt={title} />\n        </div>\n      </Link>\n      <div className={s.header}>\n        <Typography variant=\"h3\" className={s.title}>\n          {title}\n        </Typography>\n        {actions}\n      </div>\n\n      <div className={s.details}>\n        {shouldShowOwnerName && (\n          <div className={s.madeFor}>\n            <Typography variant=\"body2\" as=\"span\" className={s.madeForText}>\n              {t('playlist.made_for')}{' '}\n            </Typography>\n            <Link\n              to={`${Paths.Profile}/${userId}`}\n              className={s.userLink}\n              onClick={handleUserNameClick}>\n              {userName}\n            </Link>\n          </div>\n        )}\n\n        <div className={s.detailsRow}>\n          {tracksCount != null && (\n            <Typography variant=\"body2\" className={s.tracks}>\n              {t('playlist.tracks_count', { count: tracksCount })}\n            </Typography>\n          )}\n          {shouldShowCreatedDate && (\n            <>\n              <span className={s.dot} aria-hidden=\"true\" />\n              <Typography variant=\"body2\" className={s.created}>\n                {formatCreatedDate(addedAt)}\n              </Typography>\n            </>\n          )}\n        </div>\n      </div>\n      {/*  'reaction' in props — Type guard for correct type checking */}\n      {isShowReactionButtons && 'reaction' in props && (\n        <ReactionButtons\n          className={s.reactionButtons}\n          reaction={props.reaction}\n          onLike={() => like({ id })}\n          onDislike={() => dislike({ id })}\n          likesCount={props.likesCount}\n          onUnReaction={() => unReaction({ id })}\n        />\n      )}\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistCard/index.ts",
    "content": "export * from './PlaylistCard'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistCardSkeleton/PlaylistCardSkeleton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from '@/shared/components'\n\nimport { PlaylistCardSkeleton } from './PlaylistCardSkeleton'\n\nconst meta: Meta<typeof PlaylistCardSkeleton> = {\n  title: 'skeletons/PlaylistCardSkeleton',\n  component: PlaylistCardSkeleton,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistCardSkeleton>\n\nexport const Default: Story = {\n  args: {},\n}\n\nexport const WithReactionButtons: Story = {\n  args: {\n    showReactionButtons: true,\n  },\n}\n\nexport const AllVariants = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '24px', alignItems: 'flex-start' }}>\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '12px' }}>\n          Without reaction buttons\n        </Typography>\n        <PlaylistCardSkeleton />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '12px' }}>\n          With reaction buttons\n        </Typography>\n        <PlaylistCardSkeleton showReactionButtons />\n      </div>\n    </div>\n  ),\n}\n\nexport const GridExample = {\n  render: () => (\n    <div style={{ width: '900px' }}>\n      <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n        Playlist cards loading example\n      </Typography>\n\n      <div\n        style={{\n          display: 'grid',\n          gridTemplateColumns: 'repeat(auto-fit, minmax(264px, 1fr))',\n          gap: '16px',\n        }}>\n        {Array.from({ length: 6 }, (_, i) => (\n          <PlaylistCardSkeleton key={i} showReactionButtons={i % 2 === 0} />\n        ))}\n      </div>\n    </div>\n  ),\n}\n\nexport const ComparisonWithReal = {\n  render: () => (\n    <div style={{ width: '600px' }}>\n      <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n        Comparison with real cards\n      </Typography>\n\n      <div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>\n        <div>\n          <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n            Skeleton\n          </Typography>\n          <PlaylistCardSkeleton />\n        </div>\n\n        <div>\n          <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n            Skeleton with reactions\n          </Typography>\n          <PlaylistCardSkeleton showReactionButtons />\n        </div>\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistCardSkeleton/PlaylistCardSkeleton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport { Card, Skeleton } from '@/shared/components'\n\nimport s from '../PlaylistCard/PlaylistCard.module.css'\n\nexport type PlaylistCardSkeletonProps = {\n  showReactionButtons?: boolean\n  className?: string\n} & ComponentProps<'div'>\n\nexport const PlaylistCardSkeleton = ({\n  showReactionButtons = false,\n  className,\n  ...props\n}: PlaylistCardSkeletonProps) => {\n  return (\n    <Card\n      className={clsx(s.card, showReactionButtons && s.withReactionButtons, className)}\n      {...props}>\n      <Skeleton width=\"100%\" height={153} className={s.image} />\n\n      <Skeleton width=\"80%\" height={16} className={s.title} />\n\n      <Skeleton width=\"60%\" height={14} className={s.description} />\n\n      {showReactionButtons && (\n        <div className={s.reactionButtons}>\n          <Skeleton circle width={28} height={28} />\n          <Skeleton circle width={28} height={28} />\n        </div>\n      )}\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistCardSkeleton/index.ts",
    "content": "export * from './PlaylistCardSkeleton'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.module.css",
    "content": ".container {\n  display: flex;\n  gap: 24px;\n  background: transparent;\n}\n\n.imageContainer {\n  flex-shrink: 0;\n  width: 297px;\n  height: 297px;\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  box-shadow: 0 4px 60px rgba(0, 0, 0, 0.5);\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  min-width: 0;\n  justify-content: flex-end;\n}\n\n.title {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  margin-bottom: 8px;\n\n  font-size: clamp(var(--font-size-xxl), 8vw, var(--font-size-xxxl));\n  font-weight: 900;\n  white-space: pre-wrap;\n  line-height: 1;\n}\n\n.description {\n  opacity: 0.7;\n  margin-bottom: 8px;\n}\n\n.info {\n  margin-top: 8px;\n  overflow-wrap: break-word;\n}\n\n.meta {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n}\n\n.dot {\n  opacity: 0.7;\n}\n\n.userName strong {\n  font-weight: 600;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_5_HASHTAGS } from '@/features/tags'\n\nimport { PlaylistOverview } from '../PlaylistOverview'\n\nconst meta: Meta<typeof PlaylistOverview> = {\n  title: 'entities/PlaylistOverview',\n  component: PlaylistOverview,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistOverview>\n\nexport const Default: Story = {\n  args: {\n    title: 'Chill Mixg',\n    image: 'https://unsplash.it/297/297',\n    description: 'Julia Wolf, ayokay, Khalid and more',\n    tags: MOCK_5_HASHTAGS.map((tag) => ({ id: tag.id, name: tag.name })),\n  },\n}\n\nexport const LongTitle: Story = {\n  args: {\n    title: 'This is a Very Long Playlist Title That Should Scale Responsively',\n    image: 'https://unsplash.it/299/299',\n    description: 'A collection of amazing tracks from various artists around the world',\n    tags: MOCK_5_HASHTAGS.map((tag) => ({ id: tag.id, name: tag.name })),\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.tsx",
    "content": "import clsx from 'clsx'\nimport { type ComponentProps } from 'react'\nimport { useTranslation } from 'react-i18next'\n\nimport { TagsList } from '@/features/tags'\nimport type { Tag } from '@/features/tags/api/tagsApi.types'\nimport Image from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { Typography } from '@/shared/components'\n\nimport s from './PlaylistOverview.module.css'\n\ntype PlaylistOverviewProps = {\n  title: string\n  image?: string\n  description: string\n  tags: Tag[]\n  userName?: string\n  tracksCount?: number\n} & ComponentProps<'div'>\n\nexport const PlaylistOverview = ({\n  title,\n  image = Image,\n  description,\n  tags,\n  className,\n  userName,\n  tracksCount,\n  ...props\n}: PlaylistOverviewProps) => {\n  const { t } = useTranslation()\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <div className={s.imageContainer}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <div className={s.content}>\n        <TagsList tags={tags} entity=\"playlists\" />\n\n        <Typography variant=\"h1\" as=\"h1\" className={s.title}>\n          {title}\n        </Typography>\n\n        <div className={s.info}>\n          <Typography variant=\"body1\" className={s.description}>\n            {description}\n          </Typography>\n          <div className={s.meta}>\n            {userName && (\n              <Typography variant=\"body2\" as=\"span\" className={s.userName}>\n                {t('playlist.made_for')} <strong>{userName}</strong>\n              </Typography>\n            )}\n            {tracksCount !== undefined && (\n              <>\n                <span className={s.dot}>•</span>\n                <Typography variant=\"body2\" as=\"span\" className={s.tracksCount}>\n                  {t('playlist.tracks_count', { count: tracksCount })}\n                </Typography>\n              </>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistOverview/index.ts",
    "content": "export * from './PlaylistOverview'\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistRow/PlaylistRow.module.css",
    "content": ".playlistRow {\n  display: flex;\n  gap: 20px;\n  align-items: center;\n\n  width: 100%;\n  padding: 8px 16px;\n}\n\n.playlistLink {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  min-width: 400px;\n}\n\n.playlistRow:hover {\n  border-radius: 4px;\n  background-color: var(--color-accent);\n}\n\n.image {\n  width: 52px;\n  height: 52px;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  flex-grow: 1;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.trackCounts {\n  cursor: default;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/PlaylistRow/PlaylistRow.tsx",
    "content": "import clsx from 'clsx'\nimport { Link } from 'react-router'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { Typography } from '@/shared/components'\nimport { Paths } from '@/shared/configs'\nimport s from './PlaylistRow.module.css'\nimport { useTranslation } from 'react-i18next'\n\ntype PlaylistRowProps = {\n  id: string\n  title: string\n  imageSrc?: string\n  trackCount?: number\n  className?: string\n}\n\nexport const PlaylistRow = ({\n  title,\n  imageSrc = noCoverPlaceholder,\n  id,\n  className,\n}: PlaylistRowProps) => {\n  const { t } = useTranslation()\n  return (\n    <div className={clsx(s.playlistRow, className)}>\n      <Link to={`${Paths.Playlists}/${id}`} className={s.playlistLink}>\n        <div className={s.image}>\n          <img src={imageSrc} alt={title} />\n        </div>\n\n        <div className={s.titleWrapper}>\n          <Typography variant=\"body1\" as=\"h2\" className={s.title}>\n            {title}\n          </Typography>\n        </div>\n      </Link>\n\n      <div className={s.trackCounts}>\n        <Typography variant=\"body2\" as=\"span\" className={s.trackCount}>\n          {t('playlist.tracks_count', { count: 143 })}\n        </Typography>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/playlists/ui/index.ts",
    "content": "export * from './CreateEditPlaylistModal'\nexport * from './PlaylistActions'\nexport * from './PlaylistCard'\nexport * from './PlaylistCardSkeleton'\nexport * from './PlaylistOverview'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/config/empty-profile.ts",
    "content": "import type { Profile } from '@/features/profile'\n\nexport const emptyProfile: Profile = {\n  avatar: null,\n  fullName: { name: '', surname: '' },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/config/index.ts",
    "content": "export { emptyProfile } from './empty-profile'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/index.ts",
    "content": "export * from './config'\nexport * from './model'\nexport * from './types'\nexport * from './ui'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/model/hook/index.ts",
    "content": "export * from './useEditProfileModal'\nexport * from './useEditProfileSchema'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/model/hook/useEditProfileModal.ts",
    "content": "import { useCallback } from 'react'\n\nimport { selectIsEditProfileModalOpen, setEditProfileModalOpen } from '@/features/profile'\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\n\nexport const useEditProfileModal = () => {\n  const dispatch = useAppDispatch()\n\n  const isEditProfileOpen = useAppSelector(selectIsEditProfileModalOpen)\n\n  const handleOpenEditProfileModal = useCallback(() => {\n    dispatch(setEditProfileModalOpen(true))\n  }, [dispatch])\n\n  return {\n    isEditProfileOpen,\n    handleOpenEditProfileModal,\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/model/hook/useEditProfileSchema.ts",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { editProfileSchemaBase } from '@/features/profile/model/profile-schemas'\n\nexport const useEditProfileSchema = () => {\n  const { t } = useTranslation()\n\n  const editProfileSchema = editProfileSchemaBase(t)\n\n  return { editProfileSchema }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/model/index.ts",
    "content": "export * from './hook'\nexport * from './profile-schemas'\nexport * from './profile-slice'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/model/profile-schemas.ts",
    "content": "import type { TFunction } from 'i18next'\nimport { z } from 'zod'\n\nexport const editProfileSchemaBase = (t: TFunction) =>\n  z.object({\n    name: z\n      .string()\n      .min(1, t('profile.title.required_name'))\n      .min(2, t('profile.title.min_value_name', { quantity: '2' }))\n      .max(20, t('profile.title.max_value_name', { quantity: '20' })),\n    surname: z\n      .string()\n      .min(1, t('profile.title.required_surname'))\n      .min(2, t('profile.title.min_value_surname', { quantity: '2' }))\n      .max(20, t('profile.title.max_value_surname', { quantity: '20' })),\n  })\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/model/profile-slice.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\n\nimport { localStorageKeys } from '@/app/api/base-query-with-refresh-token-flow-api'\nimport type { FullName } from '@/features/profile'\nimport { emptyProfile } from '@/features/profile'\nimport { getProfileStorageKey } from '@/features/profile/utils'\n\nconst initialState = {\n  createEditModal: {\n    isOpen: false,\n  },\n  profile: emptyProfile,\n}\n\nexport const profileSlice = createSlice({\n  name: 'profile',\n  initialState,\n  reducers: (create) => ({\n    setEditProfileModalOpen: create.reducer<boolean>((state, action) => {\n      state.createEditModal.isOpen = action.payload\n    }),\n    setProfileAvatar: create.reducer<string | null>((state, action) => {\n      state.profile.avatar = action.payload\n    }),\n    setProfileFullName: create.reducer<FullName>((state, action) => {\n      state.profile.fullName = action.payload\n    }),\n    //! FIXME: temporary implementation until backend issue #160 is fixed\n    hydrateProfileFromStorage: create.reducer<{ userId?: string }>((state, action) => {\n      const hasToken = !!localStorage.getItem(localStorageKeys.accessToken)\n      if (!hasToken || !action.payload.userId) {\n        state.profile = emptyProfile\n        return\n      }\n\n      const stored = localStorage.getItem(getProfileStorageKey(action.payload.userId))\n      if (stored) {\n        state.profile = JSON.parse(stored)\n      }\n    }),\n  }),\n  selectors: {\n    selectIsEditProfileModalOpen: (state) => state.createEditModal.isOpen,\n    selectProfileAvatar: (state) => state.profile.avatar,\n    selectProfileFullName: (state) => state.profile.fullName,\n  },\n})\n\nexport const {\n  setEditProfileModalOpen,\n  setProfileAvatar,\n  setProfileFullName,\n  hydrateProfileFromStorage,\n} = profileSlice.actions\nexport const { selectIsEditProfileModalOpen, selectProfileAvatar, selectProfileFullName } =\n  profileSlice.selectors\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/types/index.ts",
    "content": "export * from './profile.type'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/types/profile.type.ts",
    "content": "export type FullName = {\n  name: string\n  surname: string\n}\n\nexport type Profile = {\n  fullName: FullName\n  avatar: string | null\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/ui/EditProfileModal/EditProfileModal.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 745px;\n}\n\n.dialog [data-testid='container'] > * {\n  border-radius: 50%;\n}\n\n.form {\n  overflow-y: auto;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n  margin-bottom: 16px;\n}\n\n.imageUploader {\n  width: 280px;\n  margin: 0 auto;\n}\n\n.imageUploader > label,\n.imageUploader div {\n  overflow: hidden;\n  border-radius: 50%;\n}\n\n.imageUploader button {\n  top: 40px;\n  right: 40px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/ui/EditProfileModal/EditProfileModal.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod'\nimport { useState } from 'react'\nimport { useForm } from 'react-hook-form'\nimport { useTranslation } from 'react-i18next'\n\nimport { useMeQuery } from '@/features/auth'\nimport type { Profile } from '@/features/profile'\nimport { setEditProfileModalOpen, useEditProfileSchema } from '@/features/profile'\nimport {\n  selectProfileAvatar,\n  selectProfileFullName,\n  setProfileAvatar,\n  setProfileFullName,\n} from '@/features/profile'\nimport { getProfileStorageKey } from '@/features/profile/utils'\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  FormControlledTextField,\n  ImageUploader,\n  Typography,\n} from '@/shared/components'\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\nimport { convertFileToBase64, showErrorToast } from '@/shared/utils'\n\nimport s from './EditProfileModal.module.css'\n\ntype FormData = {\n  name: string\n  surname: string\n}\n\nexport const EditProfileModal = () => {\n  const { t } = useTranslation()\n  const { editProfileSchema } = useEditProfileSchema()\n\n  const dispatch = useAppDispatch()\n  const { data: me } = useMeQuery()\n  const profileFullName = useAppSelector(selectProfileFullName)\n  const profileAvatarUrl = useAppSelector(selectProfileAvatar)\n\n  const [selectedImage, setSelectedImage] = useState<File | null>(null)\n\n  const {\n    control,\n    handleSubmit,\n    formState: { isSubmitting, isValid },\n  } = useForm<FormData>({\n    resolver: zodResolver(editProfileSchema),\n    defaultValues: {\n      name: profileFullName?.name || '',\n      surname: profileFullName?.surname || '',\n    },\n    mode: 'onChange',\n  })\n\n  const handleClose = () => {\n    dispatch(setEditProfileModalOpen(false))\n  }\n\n  const handleImageSelect = (file: File) => {\n    setSelectedImage(file)\n  }\n\n  const onSubmit = async (data: FormData) => {\n    try {\n      const avatarBase64 = selectedImage\n        ? await convertFileToBase64(selectedImage)\n        : profileAvatarUrl\n      const fullName = data\n\n      localStorage.setItem(\n        getProfileStorageKey(me!.userId),\n        JSON.stringify({ fullName, avatar: avatarBase64 } as Profile)\n      )\n\n      dispatch(setProfileAvatar(avatarBase64))\n      dispatch(setProfileFullName(fullName))\n\n      handleClose()\n    } catch (error) {\n      console.error(`Failed to save profile:`, error)\n      showErrorToast(`Failed to save profile`)\n    }\n  }\n\n  return (\n    <Dialog open onClose={handleClose} className={s.dialog}>\n      <DialogHeader>\n        <Typography variant=\"h2\">{t('profile.title.edit_profile')}</Typography>\n      </DialogHeader>\n\n      <form onSubmit={handleSubmit(onSubmit)} className={s.form}>\n        <DialogContent className={s.content}>\n          <ImageUploader\n            className={s.imageUploader}\n            onImageSelect={handleImageSelect}\n            initialImageUrl={profileAvatarUrl || undefined}\n            placeholder={t('profile.placeholder.upload_avatar')}\n          />\n\n          <FormControlledTextField\n            control={control}\n            name=\"name\"\n            label={t('profile.label.name')}\n            placeholder={t('profile.placeholder.enter_profile_name')}\n          />\n\n          <FormControlledTextField\n            control={control}\n            name=\"surname\"\n            label={t('profile.label.surname')}\n            placeholder={t('profile.placeholder.enter_profile_surname')}\n          />\n        </DialogContent>\n\n        <DialogFooter>\n          <Button variant=\"secondary\" onClick={handleClose} type=\"button\" disabled={isSubmitting}>\n            {t('button.cancel')}\n          </Button>\n          <Button variant=\"primary\" type=\"submit\" disabled={isSubmitting || !isValid}>\n            {isSubmitting ? t('button.saving') : t('button.save_changes')}\n          </Button>\n        </DialogFooter>\n      </form>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/ui/EditProfileModal/index.ts",
    "content": "export { EditProfileModal } from './EditProfileModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/ui/index.ts",
    "content": "export * from './EditProfileModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/utils/index.ts",
    "content": "export * from './storage-key'\n"
  },
  {
    "path": "apps/rtk-query/src/features/profile/utils/storage-key.ts",
    "content": "export const getProfileStorageKey = (userId: string) => `profile_${userId}`\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/api/index.ts",
    "content": "export * from './tagsApi'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/api/tagsApi.ts",
    "content": "export const MOCK_HASHTAGS: Tag[] = [\n  { id: '1', name: 'Rock' },\n  { id: '2', name: 'Jazz' },\n  { id: '3', name: 'Blues' },\n  { id: '4', name: 'Metal' },\n  { id: '5', name: 'Folk' },\n  { id: '6', name: 'Coding' },\n  { id: '7', name: 'Dark Ambient' },\n  { id: '8', name: 'Chill' },\n  { id: '9', name: 'Lo-fi' },\n]\n\nexport const MOCK_5_HASHTAGS = MOCK_HASHTAGS.slice(0, 5)\n\nexport type TagDto = {\n  id: string\n  name: string\n}\n\nimport { baseApi } from '@/app/api/base-api.ts'\n\nimport type { GetTagResponse, GetTagsResponse, Tag } from './tagsApi.types.ts'\n\nexport const tagsApi = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    findTags: build.query<Tag[], { value: string }>({\n      query: ({ value }) => `/tags/search?search=${value}`,\n      transformResponse: (response: GetTagsResponse) =>\n        response.data.map((tag) => ({\n          id: tag.id,\n          name: tag.attributes.name,\n        })),\n      providesTags: ['Tag'],\n    }),\n    createTag: build.mutation<GetTagResponse, { name: string }>({\n      query: ({ name }) => ({\n        url: '/tags',\n        method: 'POST',\n        body: {\n          data: {\n            type: 'tags',\n            attributes: { name },\n          },\n        },\n      }),\n      invalidatesTags: ['Tag'],\n    }),\n    removeTag: build.mutation<Tag, { id: string }>({\n      query: (body) => ({ url: `/tags/${body.id}`, method: 'DELETE', body }),\n      invalidatesTags: ['Tag'],\n    }),\n  }),\n})\n\nexport const { useFindTagsQuery, useCreateTagMutation, useRemoveTagMutation } = tagsApi\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/api/tagsApi.types.ts",
    "content": "export type Tag = {\n  id: string\n  name: string\n}\n\n// JSON:API format types\nexport type TagAttributes = {\n  name: string\n}\n\nexport type TagResource = {\n  id: string\n  type: 'tags'\n  attributes: TagAttributes\n}\n\nexport type GetTagsResponse = {\n  data: TagResource[]\n}\n\nexport type GetTagResponse = {\n  data: TagResource\n}\n\nexport type CreateTagRequest = {\n  data: {\n    type: 'tags'\n    attributes: TagAttributes\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/ui/PlaylistTagAutocomplete/PlaylistTagAutocomplete.module.css",
    "content": ""
  },
  {
    "path": "apps/rtk-query/src/features/tags/ui/PlaylistTagAutocomplete/PlaylistTagAutocomplete.tsx",
    "content": "import { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\n\nimport { useFindTagsQuery } from '@/features/tags'\nimport { Autocomplete } from '@/shared/components'\n\nexport const PlaylistTagAutocomplete = ({\n  value,\n  onChange,\n}: {\n  value: string[]\n  onChange: (value: string[]) => void\n}) => {\n  const { t } = useTranslation()\n\n  const [searchTerm, setSearchTerm] = useState('')\n  const { data: tags } = useFindTagsQuery({ value: searchTerm })\n\n  const options =\n    tags?.map((tag) => ({\n      label: tag.name,\n      value: tag.id,\n    })) ?? []\n\n  return (\n    <Autocomplete\n      label={t('tags.label')}\n      value={value}\n      searchTerm={searchTerm}\n      setSearchTerm={setSearchTerm}\n      onChange={onChange}\n      maxTags={5}\n      options={options}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/ui/PlaylistTagAutocomplete/index.ts",
    "content": "export * from './PlaylistTagAutocomplete.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/ui/TagsList/TagsList.module.css",
    "content": ".list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/ui/TagsList/TagsList.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_HASHTAGS } from '@/features/tags'\n\nimport { TagsList } from './TagsList'\n\nconst meta: Meta<typeof TagsList> = {\n  title: 'entities/TagsList',\n  component: TagsList,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TagsList>\n\nexport const Default: Story = {\n  args: {\n    tags: MOCK_HASHTAGS,\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/ui/TagsList/TagsList.tsx",
    "content": "import { Link } from 'react-router'\n\nimport { Tag } from '@/shared/components'\n\nimport type { Tag as TagType } from '../../api/tagsApi.types'\nimport s from './TagsList.module.css'\n\nexport const TagsList = ({\n  tags,\n  entity = 'tracks',\n}: {\n  tags: TagType[]\n  entity?: 'tracks' | 'playlists'\n}) => {\n  return (\n    <ul className={s.list}>\n      {tags.map((tag) => (\n        <li key={tag.id}>\n          <Tag as={Link} to={`/${entity}?tags=${tag.id}`} tag={tag.name} />\n        </li>\n      ))}\n    </ul>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/ui/TagsList/index.ts",
    "content": "export { TagsList } from './TagsList'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tags/ui/index.ts",
    "content": "export * from './PlaylistTagAutocomplete'\nexport * from './TagsList'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/api/index.ts",
    "content": "export * from './tracksApi'\nexport * from './tracksApi.types'\nexport { MOCK_TRACKS } from '@/features/tracks/api/mocks.ts'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/api/mocks.ts",
    "content": "enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = -1,\n}\n\nexport const MOCK_TRACKS = [\n  {\n    id: '1',\n    type: 'tracks',\n    attributes: {\n      artist: 'Headlund',\n      id: '1',\n      title: 'Days That Matter',\n      addedAt: '2025-06-01T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/110/110',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 104,\n      dislikesCount: 2,\n      artists: [{ id: '1', name: 'John Doe' }],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '2',\n    type: 'tracks',\n    attributes: {\n      artist: 'Stellar Wave',\n      id: '2',\n      title: 'Cosmic Dust',\n      addedAt: '2025-06-02T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/111/111',\n          },\n        ],\n      },\n      user: {\n        id: '2',\n        name: 'Jane Smith',\n      },\n      currentUserReaction: CurrentUserReaction.Like,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '2', name: 'Jane Smith' }],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '3',\n    type: 'tracks',\n    attributes: {\n      artist: 'Aqua Marine',\n      id: '3',\n      title: 'Ocean Breath Is The Best Track Ever',\n      addedAt: '2025-06-03T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/112/112',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 1,\n      dislikesCount: 2,\n      artists: [\n        { id: '3', name: 'Peter Jones' },\n        { id: '4', name: 'Chris Green' },\n        { id: '5', name: 'John Doe' },\n      ],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '4',\n    type: 'tracks',\n    attributes: {\n      artist: 'Night Rider',\n      id: '4',\n      title: 'Midnight Drive',\n      addedAt: '2025-06-04T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/113/113',\n          },\n        ],\n      },\n      user: {\n        id: '3',\n        name: 'Peter Jones',\n      },\n      currentUserReaction: CurrentUserReaction.Dislike,\n      likesCount: 666,\n      dislikesCount: 2,\n      artists: [{ id: '4', name: 'Chris Green' }],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '5',\n    type: 'tracks',\n    attributes: {\n      artist: 'Urban Glow',\n      id: '5',\n      title: 'City Lights',\n      addedAt: '2025-06-05T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/114/114',\n          },\n        ],\n      },\n      user: {\n        id: '2',\n        name: 'Jane Smith',\n      },\n      currentUserReaction: CurrentUserReaction.Like,\n      likesCount: 8,\n      dislikesCount: 2,\n      artists: [{ id: '5', name: 'John Doe' }],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '6',\n    type: 'tracks',\n    attributes: {\n      artist: 'Whispering Pines',\n      id: '6',\n      title: 'Forest Lullaby',\n      addedAt: '2025-06-06T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/115/115',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 1,\n      dislikesCount: 2,\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '7',\n    type: 'tracks',\n    attributes: {\n      artist: 'Sandstorm',\n      id: '7',\n      title: 'Desert Mirage',\n      addedAt: '2025-06-07T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/116/116',\n          },\n        ],\n      },\n      user: {\n        id: '4',\n        name: 'Susan Lee',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '7', name: 'John Doe' }],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '8',\n    type: 'tracks',\n    attributes: {\n      artist: 'Altitude',\n      id: '8',\n      title: 'Mountain Peak',\n      addedAt: '2025-06-08T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/117/117',\n          },\n        ],\n      },\n      user: {\n        id: '3',\n        name: 'Peter Jones',\n      },\n      currentUserReaction: CurrentUserReaction.Like,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '8', name: 'John Doe' }],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '9',\n    type: 'tracks',\n    attributes: {\n      artist: 'Water Lily',\n      id: '9',\n      title: 'River Flow',\n      addedAt: '2025-06-09T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/118/118',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: CurrentUserReaction.Dislike,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '10', name: 'John Doe' }],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n  {\n    id: '10',\n    type: 'tracks',\n    attributes: {\n      artist: 'Galaxy Explorer',\n      id: '10',\n      title: 'Final Frontier',\n      addedAt: '2025-06-10T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/119/119',\n          },\n        ],\n      },\n      user: {\n        id: '5',\n        name: 'Chris Green',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '10', name: 'John Doe' }],\n      duration: 100,\n      isPublished: true,\n    },\n  },\n]\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/api/tracksApi.ts",
    "content": "import { baseApi } from '@/app/api/base-api.ts'\nimport { FETCH_TRACK_BY_SCROLL_PAGE_SIZE } from '@/features/tracks/constants'\nimport { CurrentUserReaction, type Nullable, type ReactionResponse } from '@/shared/types'\nimport { buildQueryString } from '@/shared/utils'\n\nimport type {\n  FetchPlaylistsTracksResponse,\n  FetchTrackByIdResponse,\n  FetchTracksArgs,\n  FetchTracksResponse,\n  TrackDetailAttributes,\n  TrackDetails,\n  UpdateTrackArgs,\n} from './tracksApi.types.ts'\n\nexport const tracksAPI = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    fetchTracksByScroll: build.infiniteQuery<\n      FetchTracksResponse,\n      FetchTracksArgs | void,\n      string | undefined\n    >({\n      infiniteQueryOptions: {\n        initialPageParam: undefined,\n        getNextPageParam: (lastPage) => {\n          return lastPage.meta.nextCursor || null\n        },\n      },\n\n      query: (args) => {\n        const { pageParam, ...params } = args as any\n        return {\n          url: 'playlists/tracks',\n          params: {\n            cursor: pageParam,\n            paginationType: 'cursor',\n            pageSize: FETCH_TRACK_BY_SCROLL_PAGE_SIZE,\n            ...params,\n          },\n        }\n      },\n      providesTags: (result) =>\n        result\n          ? [\n              ...result.pages.flatMap((page) =>\n                page.data.map((track) => ({ type: 'Track' as const, id: track.id }))\n              ),\n              { type: 'Track', id: 'LIST' },\n            ]\n          : [{ type: 'Track', id: 'LIST' }],\n    }),\n    fetchTracks: build.query<FetchTracksResponse, FetchTracksArgs>({\n      query: (params) => {\n        const query = buildQueryString(params) // TODO: возможно, это излишне\n\n        return `playlists/tracks?${query}`\n      },\n      providesTags: (result) => [\n        ...(result?.data.map((track) => {\n          return { type: 'Track' as const, id: track.id }\n        }) || []),\n        'Track',\n      ],\n    }),\n    fetchTracksInPlaylist: build.query<\n      FetchPlaylistsTracksResponse,\n      FetchTracksArgs & { playlistId: string }\n    >({\n      query: ({ playlistId, ...params }) => ({\n        url: `playlists/${playlistId}/tracks`,\n        params: params,\n      }),\n      providesTags: (res) => res?.data.map((track) => ({ type: 'Track', trackId: track.id })) || [],\n    }),\n    fetchTrackById: build.query<FetchTrackByIdResponse, { trackId: string }>({\n      query: ({ trackId }) => ({\n        url: `playlists/tracks/${trackId}`,\n      }),\n      providesTags: (_, __, { trackId }) => [{ type: 'Track', id: trackId }],\n    }),\n    createTrack: build.mutation<\n      { data: TrackDetails<TrackDetailAttributes> },\n      { title: string; file: File }\n    >({\n      query: ({ title, file }) => {\n        const formData = new FormData()\n        formData.append('data[type]', 'tracks')\n        formData.append('data[attributes][title]', title)\n        formData.append('file', file)\n\n        return {\n          url: `playlists/tracks/upload`,\n          method: 'POST',\n          body: formData,\n        }\n      },\n      invalidatesTags: ['Track'],\n    }),\n    updateTrack: build.mutation<\n      TrackDetails<TrackDetailAttributes>,\n      { trackId: string; payload: UpdateTrackArgs }\n    >({\n      query: ({ trackId, payload }) => ({\n        url: `playlists/tracks/${trackId}`,\n        method: 'PUT',\n        body: {\n          data: {\n            type: 'tracks',\n            attributes: payload,\n          },\n        },\n      }),\n\n      invalidatesTags: ['Track'],\n    }),\n    addTrackToPlaylist: build.mutation<void, { playlistId: string; trackId: string }>({\n      query: ({ trackId, playlistId }) => ({\n        url: `playlists/${playlistId}/relationships/tracks`,\n        method: 'POST',\n        body: {\n          data: {\n            type: 'playlist-tracks',\n            attributes: {\n              trackId: trackId,\n            },\n          },\n        },\n      }),\n      invalidatesTags: ['Track', 'Playlist'],\n    }),\n    removeTrackFromPlaylist: build.mutation<void, { playlistId: string; trackId: string }>({\n      query: ({ trackId, playlistId }) => ({\n        url: `playlists/${playlistId}/relationships/tracks/${trackId}`,\n        method: 'DELETE',\n      }),\n      invalidatesTags: (_res, __err, { playlistId, trackId }) => [\n        'Playlist',\n        { type: 'Playlist', id: playlistId },\n        { type: 'Track', id: trackId },\n      ],\n    }),\n    reorderTracks: build.mutation<\n      void,\n      {\n        trackId: string\n        playlistId: string\n        putAfterItemId: Nullable<string>\n      }\n    >({\n      query: ({ trackId, playlistId, putAfterItemId }) => ({\n        url: `playlists/${playlistId}/tracks/${trackId}/reorder`,\n        method: 'PUT',\n        body: {\n          putAfterItemId: putAfterItemId,\n        },\n      }),\n      invalidatesTags: (_res, _err, { playlistId }) => [{ type: 'Playlist', id: playlistId }],\n    }),\n    removeTrack: build.mutation<void, { trackId: string }>({\n      query: ({ trackId }) => ({\n        url: `playlists/tracks/${trackId}`,\n        method: 'DELETE',\n      }),\n      invalidatesTags: ['Track'],\n    }),\n    likeTrack: build.mutation<ReactionResponse, { trackId: string }>({\n      query: ({ trackId }) => ({\n        url: `playlists/tracks/${trackId}/likes`,\n        method: 'POST',\n      }),\n      async onQueryStarted({ trackId }, { dispatch, getState, queryFulfilled }) {\n        const patchResults: any[] = []\n\n        // Refresh the cache for a single track page (fetchTrackById)\n        const patchTrackById = dispatch(\n          tracksAPI.util.updateQueryData('fetchTrackById', { trackId }, (state) => {\n            if (state.data.attributes.currentUserReaction === CurrentUserReaction.Dislike) {\n              state.data.attributes.dislikesCount -= 1\n            }\n            state.data.attributes.likesCount += 1\n            state.data.attributes.currentUserReaction = CurrentUserReaction.Like\n          })\n        )\n        patchResults.push(patchTrackById)\n\n        // Refresh cache for infinite scroll (fetchTracksByScroll)\n        const scrollArgs = tracksAPI.util.selectCachedArgsForQuery(\n          getState(),\n          'fetchTracksByScroll'\n        )\n        if (scrollArgs) {\n          scrollArgs.forEach((scrollArg) => {\n            patchResults.push(\n              dispatch(\n                tracksAPI.util.updateQueryData('fetchTracksByScroll', scrollArg, (state) => {\n                  // Go through all pages\n                  state.pages.forEach((page) => {\n                    const track = page.data.find((t: any) => t.id === trackId)\n                    if (track) {\n                      if (track.attributes.currentUserReaction === CurrentUserReaction.Dislike) {\n                        track.attributes.dislikesCount -= 1\n                      }\n                      track.attributes.likesCount += 1\n                      track.attributes.currentUserReaction = CurrentUserReaction.Like\n                    }\n                  })\n                })\n              )\n            )\n          })\n        }\n\n        // Refresh the cache for track lists (fetchTracks)\n        const args = tracksAPI.util.selectCachedArgsForQuery(getState(), 'fetchTracks')\n        args.forEach((arg: FetchTracksArgs) => {\n          patchResults.push(\n            dispatch(\n              tracksAPI.util.updateQueryData('fetchTracks', arg || {}, (state) => {\n                const track = state.data.find((t) => t.id === trackId)\n                if (track) {\n                  if (track.attributes.currentUserReaction === CurrentUserReaction.Dislike) {\n                    track.attributes.dislikesCount -= 1\n                  }\n                  track.attributes.likesCount += 1\n                  track.attributes.currentUserReaction = CurrentUserReaction.Like\n                }\n              })\n            )\n          )\n        })\n\n        try {\n          await queryFulfilled\n        } catch {\n          patchResults.forEach((p) => p.undo())\n        }\n      },\n      invalidatesTags: (_res, _err, { trackId }) => [{ type: 'Track', id: trackId }],\n    }),\n    dislikeTrack: build.mutation<ReactionResponse, { trackId: string }>({\n      query: ({ trackId }) => ({\n        url: `playlists/tracks/${trackId}/dislikes`,\n        method: 'POST',\n      }),\n      async onQueryStarted({ trackId }, { dispatch, getState, queryFulfilled }) {\n        const patchResults: any[] = []\n\n        // --- ИСПРАВЛЕНИЕ: Обновляем кеш для страницы одного трека (fetchTrackById) ---\n        const patchTrackById = dispatch(\n          tracksAPI.util.updateQueryData('fetchTrackById', { trackId }, (state) => {\n            if (state.data.attributes.currentUserReaction === CurrentUserReaction.Like) {\n              state.data.attributes.likesCount -= 1\n            }\n            state.data.attributes.dislikesCount += 1\n            state.data.attributes.currentUserReaction = CurrentUserReaction.Dislike\n          })\n        )\n        patchResults.push(patchTrackById)\n\n        // Refresh cache for infinite scroll (fetchTracksByScroll)\n        const scrollArgs = tracksAPI.util.selectCachedArgsForQuery(\n          getState(),\n          'fetchTracksByScroll'\n        )\n        if (scrollArgs) {\n          scrollArgs.forEach((scrollArg) => {\n            patchResults.push(\n              dispatch(\n                tracksAPI.util.updateQueryData('fetchTracksByScroll', scrollArg, (state) => {\n                  // Go through all pages\n                  state.pages.forEach((page) => {\n                    const track = page.data.find((t: any) => t.id === trackId)\n                    if (track) {\n                      if (track.attributes.currentUserReaction === CurrentUserReaction.Like) {\n                        track.attributes.likesCount -= 1\n                      }\n                      track.attributes.dislikesCount += 1\n                      track.attributes.currentUserReaction = CurrentUserReaction.Dislike\n                    }\n                  })\n                })\n              )\n            )\n          })\n        }\n\n        // Refresh the cache for track lists (fetchTracks)\n        const args = tracksAPI.util.selectCachedArgsForQuery(getState(), 'fetchTracks')\n        args.forEach((arg: FetchTracksArgs) => {\n          patchResults.push(\n            dispatch(\n              tracksAPI.util.updateQueryData('fetchTracks', arg || {}, (state) => {\n                const track = state.data.find((t) => t.id === trackId)\n                if (track) {\n                  if (track.attributes.currentUserReaction === CurrentUserReaction.Like) {\n                    track.attributes.likesCount -= 1\n                  }\n                  track.attributes.dislikesCount += 1\n                  track.attributes.currentUserReaction = CurrentUserReaction.Dislike\n                }\n              })\n            )\n          )\n        })\n\n        try {\n          await queryFulfilled\n        } catch {\n          patchResults.forEach((p) => p.undo())\n        }\n      },\n      invalidatesTags: (_res, _err, { trackId }) => [{ type: 'Track', id: trackId }],\n    }),\n    unReactionTrack: build.mutation<ReactionResponse, { trackId: string }>({\n      query: ({ trackId }) => ({\n        url: `playlists/tracks/${trackId}/reactions`,\n        method: 'DELETE',\n      }),\n      async onQueryStarted({ trackId }, { dispatch, getState, queryFulfilled }) {\n        const patchResults: any[] = []\n\n        // Refresh the cache for a single track page (fetchTrackById)\n        const patchTrackById = dispatch(\n          tracksAPI.util.updateQueryData('fetchTrackById', { trackId }, (state) => {\n            if (state.data.attributes.currentUserReaction === CurrentUserReaction.Like) {\n              state.data.attributes.likesCount -= 1\n            } else if (state.data.attributes.currentUserReaction === CurrentUserReaction.Dislike) {\n              state.data.attributes.dislikesCount -= 1\n            }\n            state.data.attributes.currentUserReaction = CurrentUserReaction.None\n          })\n        )\n        patchResults.push(patchTrackById)\n\n        // Refresh cache for infinite scroll (fetchTracksByScroll)\n        const scrollArgs = tracksAPI.util.selectCachedArgsForQuery(\n          getState(),\n          'fetchTracksByScroll'\n        )\n        scrollArgs.forEach((scrollArg) => {\n          patchResults.push(\n            dispatch(\n              tracksAPI.util.updateQueryData('fetchTracksByScroll', scrollArg, (state) => {\n                // Go through all pages\n                state.pages.forEach((page) => {\n                  const track = page.data.find((t: any) => t.id === trackId)\n                  if (track) {\n                    if (track.attributes.currentUserReaction === CurrentUserReaction.Like) {\n                      track.attributes.likesCount -= 1\n                    } else if (\n                      track.attributes.currentUserReaction === CurrentUserReaction.Dislike\n                    ) {\n                      track.attributes.dislikesCount -= 1\n                    }\n                    track.attributes.currentUserReaction = CurrentUserReaction.None\n                  }\n                })\n              })\n            )\n          )\n        })\n\n        // Refresh the cache for track lists (fetchTracks)\n        const args = tracksAPI.util.selectCachedArgsForQuery(getState(), 'fetchTracks')\n        args.forEach((arg: FetchTracksArgs) => {\n          patchResults.push(\n            dispatch(\n              tracksAPI.util.updateQueryData('fetchTracks', arg || {}, (state) => {\n                const track = state.data.find((t) => t.id === trackId)\n                if (track) {\n                  if (track.attributes.currentUserReaction === CurrentUserReaction.Like) {\n                    track.attributes.likesCount -= 1\n                  } else if (track.attributes.currentUserReaction === CurrentUserReaction.Dislike) {\n                    track.attributes.dislikesCount -= 1\n                  }\n                  track.attributes.currentUserReaction = CurrentUserReaction.None\n                }\n              })\n            )\n          )\n        })\n\n        try {\n          await queryFulfilled\n        } catch {\n          patchResults.forEach((p) => p.undo())\n        }\n      },\n      invalidatesTags: (_res, _err, { trackId }) => [{ type: 'Track', id: trackId }],\n    }),\n    addCoverToTrack: build.mutation<void, { trackId: string; cover: File }>({\n      query: ({ trackId, cover }) => {\n        const formData = new FormData()\n        formData.append('cover', cover)\n\n        return {\n          url: `playlists/tracks/${trackId}/cover`,\n          method: 'POST',\n          body: formData,\n        }\n      },\n      invalidatesTags: (_res, _err, { trackId }) => [{ type: 'Track', id: trackId }],\n    }),\n    deleteCoverFromTrack: build.mutation<void, { trackId: string }>({\n      query: ({ trackId }) => ({\n        url: `playlists/tracks/${trackId}/cover`,\n        method: 'DELETE',\n      }),\n      invalidatesTags: (_res, _err, { trackId }) => [{ type: 'Track', id: trackId }],\n    }),\n    publishTrack: build.mutation<void, { trackId: string }>({\n      query: ({ trackId }) => ({\n        url: `playlists/tracks/${trackId}/actions/publish`,\n        method: 'POST',\n      }),\n      invalidatesTags: ['Track'],\n    }),\n  }),\n})\n\nexport const {\n  useFetchTracksByScrollInfiniteQuery,\n  useLazyFetchTrackByIdQuery,\n  useFetchTracksQuery,\n  useFetchTrackByIdQuery,\n  useAddCoverToTrackMutation,\n  useDeleteCoverFromTrackMutation,\n  useAddTrackToPlaylistMutation,\n  useCreateTrackMutation,\n  useDislikeTrackMutation,\n  useFetchTracksInPlaylistQuery,\n  useLikeTrackMutation,\n  useRemoveTrackMutation,\n  useRemoveTrackFromPlaylistMutation,\n  useUnReactionTrackMutation,\n  useUpdateTrackMutation,\n  useReorderTracksMutation,\n  usePublishTrackMutation,\n} = tracksAPI\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/api/tracksApi.types.ts",
    "content": "import type { Tag } from '@/features/tags/api/tagsApi.types.ts'\nimport {\n  CurrentUserReaction,\n  type Images,\n  type Meta,\n  type Nullable,\n  type User,\n} from '@/shared/types'\n\nexport type TrackDetails<T> = {\n  id: string\n  type: 'tracks'\n  attributes: T\n  relationships: {\n    artists: {\n      data: Array<{\n        id: string\n        type: 'artists'\n      }>\n    }\n  }\n}\n\n// Attributes\nexport type BaseAttributes = {\n  title: string\n  addedAt: string\n  attachments: TrackAttachment[]\n  images: Images\n  currentUserReaction: CurrentUserReaction\n  dislikesCount: number\n  likesCount: number\n  isPublished: boolean\n}\n\nexport type FetchTracksAttributes = BaseAttributes & {\n  user: User\n}\n\ntype Artist = {\n  id: string\n  name: string\n}\n\nexport type TrackDetailAttributes = BaseAttributes & {\n  lyrics: Nullable<string>\n  releaseDate: Nullable<string>\n  updatedAt: string\n  duration: number\n  processingStatus: TrackProcessingStatus\n  visibility: TrackVisibility\n  tags: Tag[]\n  artists: Artist[]\n  // likes\n  dislikesCount: number\n  likesCount: number\n  user: User\n}\n\nexport type PlaylistItemAttributes = BaseAttributes & {\n  updatedAt: string\n  order: number\n}\n\n// Attachment\nexport type TrackAttachment = {\n  id: string\n  addedAt: string\n  updatedAt: string\n  version: number\n  url: string\n  contentType: string\n  originalName: string\n  originalKey: string\n  fileSize: number\n}\n\n// Included\nexport type IncludedArtist = {\n  id: string\n  type: string\n  attributes: { name: string }\n}\n\n// Response\nexport type FetchTracksResponse = {\n  data: TrackDetails<FetchTracksAttributes>[]\n  meta: Meta\n  included: IncludedArtist[]\n}\n\nexport type FetchTrackByIdResponse = {\n  data: TrackDetails<TrackDetailAttributes>\n}\n\nexport type FetchPlaylistsTracksResponse = {\n  data: TrackDetails<PlaylistItemAttributes>[]\n  meta: Meta\n}\n\nexport type ApiTrack = TrackDetails<FetchTracksAttributes>\n\n// Arguments\nexport type FetchTracksArgs = {\n  pageSize?: number\n  pageNumber?: number\n  search?: string\n  sortBy?: 'addedAt' | 'likesCount'\n  sortDirection?: 'asc' | 'desc'\n  tagsIds?: string[]\n  artistsIds?: string[]\n  userId?: string\n  includeDrafts?: boolean\n}\n\nexport type UpdateTrackArgs = {\n  title?: string\n  lyrics?: string\n  visibility?: TrackVisibility\n  releaseDate?: string\n  tagIds?: string[]\n  artistsIds?: string[]\n}\n\n// Literal types\ntype TrackVisibility = 'private' | 'public'\n\ntype TrackProcessingStatus = 'uploaded' | 'converting' | 'ready'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/constants/index.ts",
    "content": "export const FETCH_TRACK_BY_SCROLL_PAGE_SIZE = 10\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/index.ts",
    "content": "export * from './api'\nexport * from './model'\nexport * from './ui'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/model/hooks/index.ts",
    "content": "export * from './useCreateTrackModal'\nexport * from './useEditTrackModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/model/hooks/useCreateTrackModal.ts",
    "content": "import { useCallback } from 'react'\n\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\n\nimport { openCreateTrackModal, selectIsCreateEditTrackModalOpen } from '../tracks-slice'\n\nexport const useCreateTrackModal = () => {\n  const dispatch = useAppDispatch()\n  const isCreateTrackModalOpen = useAppSelector(selectIsCreateEditTrackModalOpen)\n\n  const handleOpenCreateTrackModal = useCallback(() => {\n    dispatch(openCreateTrackModal())\n  }, [dispatch])\n\n  return {\n    isCreateTrackModalOpen,\n    handleOpenCreateTrackModal,\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/model/hooks/useEditTrackModal.ts",
    "content": "import { useCallback } from 'react'\n\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\n\nimport {\n  openEditTrackModal,\n  selectEditingTrackId,\n  selectIsCreateEditTrackModalOpen,\n} from '../tracks-slice'\n\nexport const useEditTrackModal = () => {\n  const dispatch = useAppDispatch()\n  const isCreateEditTrackModalOpen = useAppSelector(selectIsCreateEditTrackModalOpen)\n  const editingTrackId = useAppSelector(selectEditingTrackId)\n\n  const handleOpenEditTrackModal = useCallback(\n    (trackId: string) => {\n      dispatch(openEditTrackModal(trackId))\n    },\n    [dispatch]\n  )\n\n  return {\n    isEditTrackModalOpen: isCreateEditTrackModalOpen && editingTrackId,\n    handleOpenEditTrackModal,\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/model/index.ts",
    "content": "export * from './hooks'\nexport * from './tracks-slice'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/model/tracks-slice.ts",
    "content": "import { createSlice, type PayloadAction } from '@reduxjs/toolkit'\n\nconst initialState = {\n  createEditModal: {\n    isOpen: false,\n    trackId: null as string | null,\n  },\n}\n\nexport const tracksSlice = createSlice({\n  name: 'tracks',\n  initialState,\n  reducers: {\n    openCreateTrackModal: (state) => {\n      state.createEditModal.isOpen = true\n      state.createEditModal.trackId = null\n    },\n    openEditTrackModal: (state, action: PayloadAction<string>) => {\n      state.createEditModal.isOpen = true\n      state.createEditModal.trackId = action.payload\n    },\n    closeCreateEditTrackModal: (state) => {\n      state.createEditModal.isOpen = false\n      state.createEditModal.trackId = null\n    },\n  },\n  selectors: {\n    selectCreateEditTrackModal: (state) => state.createEditModal,\n    selectIsCreateEditTrackModalOpen: (state) => state.createEditModal.isOpen,\n    selectEditingTrackId: (state) => state.createEditModal.trackId,\n  },\n})\n\nexport const { openCreateTrackModal, openEditTrackModal, closeCreateEditTrackModal } =\n  tracksSlice.actions\nexport const {\n  selectCreateEditTrackModal,\n  selectIsCreateEditTrackModalOpen,\n  selectEditingTrackId,\n} = tracksSlice.selectors\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/CreateEditTrackModal/CreateEditTrackModal.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 745px;\n}\n\n.uploadButton {\n  width: 200px;\n  margin: 0 auto;\n}\n\n.form {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n  margin-bottom: 16px;\n}\n\n.imageUploader {\n  width: 280px;\n  margin: 0 auto;\n}\n\n.buttonsRow {\n  display: flex;\n  justify-content: space-between;\n  margin-top: 16px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/CreateEditTrackModal/CreateEditTrackModal.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { useTranslation } from 'react-i18next'\n\nimport { ArtistsTagAutocomplete } from '@/features/artists/ui'\nimport { useFetchPlaylistsQuery } from '@/features/playlists'\nimport { ChoosePlaylistButtonAndModal } from '@/features/playlists/ui/ChoosePlaylistButtonAndModal'\nimport { PlaylistTagAutocomplete } from '@/features/tags/ui/PlaylistTagAutocomplete/PlaylistTagAutocomplete'\nimport {\n  useAddCoverToTrackMutation,\n  useAddTrackToPlaylistMutation,\n  useCreateTrackMutation,\n  useFetchTrackByIdQuery,\n  useRemoveTrackFromPlaylistMutation,\n  useUpdateTrackMutation,\n} from '@/features/tracks'\nimport { closeCreateEditTrackModal, selectEditingTrackId } from '@/features/tracks'\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  FileUploader,\n  ImageUploader,\n  Textarea,\n  TextField,\n} from '@/shared/components'\nimport { Typography } from '@/shared/components/Typography/Typography'\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { addTrackToPlaylists, syncTrackPlaylists } from '../../utils/playlistSync'\nimport s from './CreateEditTrackModal.module.css'\n\n/**\n * Создание трека:\n * 1. Выбор файла + кнопка отправить\n * 2. После отправки файла мы получаем id трека, и отображаем другие поля формы:\n * - ImageUploader\n * - Title\n * - плейлисты\n * - теги\n * - Lyrics\n * 3. При нажатии на кнопку сохранения, мы отправляем данные на сервер\n * - Загружаем изображение\n * - Добавляем трек в каждый из выбранных плейлистов\n * - Обновляем данные по треку (заголовок, теги, текс)\n */\n\ntype FormData = {\n  title: string\n  lyrics: string\n  playlistIds: string[]\n  tagIds: string[]\n  artistsIds: string[]\n  releaseDate: string\n}\n\nexport const CreateEditTrackModal = () => {\n  const { t } = useTranslation()\n\n  const dispatch = useAppDispatch()\n  const [selectedFile, setSelectedFile] = useState<File | null>(null)\n  const [trackId, setTrackId] = useState<string | null>(null)\n  const [selectedImage, setSelectedImage] = useState<File | null>(null)\n\n  const [createTrack] = useCreateTrackMutation()\n  const [updateTrack] = useUpdateTrackMutation()\n  const [addTrackToPlaylist] = useAddTrackToPlaylistMutation()\n  const [removeTrackFromPlaylist] = useRemoveTrackFromPlaylistMutation()\n  const [addCoverToTrack] = useAddCoverToTrackMutation()\n\n  const editingTrackId = useAppSelector(selectEditingTrackId)\n\n  const isEditMode = Boolean(editingTrackId)\n\n  const { data: trackData } = useFetchTrackByIdQuery(\n    { trackId: editingTrackId! },\n    {\n      skip: !editingTrackId,\n    }\n  )\n\n  const trackCoverUrl = getImageByType(\n    trackData?.data.attributes.images || { main: [] },\n    ImageType.ORIGINAL\n  )?.url\n\n  const { data: playlists } = useFetchPlaylistsQuery(\n    {\n      trackId: editingTrackId!,\n    },\n    {\n      skip: !editingTrackId,\n    }\n  )\n\n  const handleFileSelect = (file: File) => {\n    setSelectedFile(file)\n  }\n\n  const handleImageSelect = (file: File) => {\n    setSelectedImage(file)\n  }\n\n  const handleUpload = () => {\n    if (!selectedFile) {\n      return\n    }\n    createTrack({\n      title: selectedFile.name,\n      file: selectedFile,\n    })\n      .unwrap()\n      .then((res) => {\n        setTrackId(res?.data?.id)\n      })\n  }\n\n  const handleClose = () => {\n    dispatch(closeCreateEditTrackModal())\n  }\n\n  const {\n    register,\n    handleSubmit,\n    control,\n    reset,\n    formState: { errors },\n  } = useForm<FormData>({\n    defaultValues: {\n      title: '',\n      lyrics: '',\n      playlistIds: [],\n      tagIds: [],\n      artistsIds: [],\n      releaseDate: new Date().toISOString(),\n    },\n  })\n\n  // Initial values\n  useEffect(() => {\n    if (isEditMode && trackData?.data) {\n      const track = trackData.data.attributes\n      reset({\n        title: track.title,\n        lyrics: track.lyrics || '',\n        tagIds: track.tags.map((tag) => tag.id),\n        artistsIds: track.artists.map((artist) => artist.id),\n        playlistIds: playlists?.data.map((playlist) => playlist.id) || [],\n        releaseDate: track.releaseDate || new Date().toISOString(),\n      })\n    }\n  }, [isEditMode, trackData, reset, playlists])\n\n  const onSubmit = async (data: FormData) => {\n    if (!trackId && !isEditMode) {\n      return\n    }\n\n    const trackIdToUpdate = trackId || editingTrackId!\n\n    try {\n      const promises: Promise<unknown>[] = []\n\n      promises.push(\n        updateTrack({\n          trackId: trackIdToUpdate,\n          payload: data,\n        }).unwrap()\n      )\n\n      // Синхронизация плейлистов\n      if (isEditMode && playlists?.data) {\n        const originalPlaylistIds = playlists.data.map((playlist) => playlist.id)\n        const newPlaylistIds = data.playlistIds\n\n        promises.push(\n          syncTrackPlaylists({\n            originalPlaylistIds,\n            newPlaylistIds,\n            trackId: trackIdToUpdate,\n            addTrackToPlaylist: (params) => addTrackToPlaylist(params).unwrap(),\n            removeTrackFromPlaylist: (params) => removeTrackFromPlaylist(params).unwrap(),\n          })\n        )\n      } else if (!isEditMode) {\n        // Для нового трека просто добавляем во все выбранные плейлисты\n        promises.push(\n          addTrackToPlaylists(trackIdToUpdate, data.playlistIds, (params) =>\n            addTrackToPlaylist(params).unwrap()\n          )\n        )\n      }\n\n      if (selectedImage) {\n        promises.push(\n          addCoverToTrack({\n            trackId: trackIdToUpdate,\n            cover: selectedImage,\n          }).unwrap()\n        )\n      }\n\n      try {\n        await Promise.all(promises)\n      } catch (error) {\n        console.error('Failed to perform some track actions:', error)\n      }\n      dispatch(closeCreateEditTrackModal())\n    } catch (error) {\n      console.error('Error saving track:', error)\n    }\n  }\n\n  return (\n    <Dialog open={true} onClose={handleClose} className={s.dialog}>\n      <DialogHeader>\n        <Typography variant=\"h2\">\n          {isEditMode ? t('tracks.title.edit') : t('tracks.title.create')}\n        </Typography>\n      </DialogHeader>\n      <DialogContent className={s.content}>\n        {!isEditMode && (\n          <>\n            <FileUploader onFileSelect={handleFileSelect} />\n            <Button\n              className={s.uploadButton}\n              onClick={handleUpload}\n              disabled={Boolean(trackId) || !selectedFile}>\n              {t('tracks.button.upload')}\n            </Button>\n          </>\n        )}\n\n        {(trackId || isEditMode) && (\n          <div>\n            <ImageUploader\n              onImageSelect={handleImageSelect}\n              className={s.imageUploader}\n              enableCrop\n              cropShape=\"rect\"\n              initialImageUrl={isEditMode ? trackCoverUrl : undefined}\n            />\n\n            <form className={s.form} onSubmit={handleSubmit(onSubmit)}>\n              <TextField\n                label={t('tracks.label.title')}\n                placeholder={t('tracks.placeholder.title')}\n                {...register('title')}\n                errorMessage={errors.title?.message}\n              />\n\n              <Controller\n                control={control}\n                name=\"artistsIds\"\n                render={({ field }) => (\n                  <ArtistsTagAutocomplete value={field.value} onChange={field.onChange} />\n                )}\n              />\n\n              <Controller\n                control={control}\n                name=\"tagIds\"\n                render={({ field }) => (\n                  <PlaylistTagAutocomplete value={field.value} onChange={field.onChange} />\n                )}\n              />\n\n              <Textarea\n                label={t('tracks.label.lyrics')}\n                placeholder={t('tracks.placeholder.lyrics')}\n                {...register('lyrics')}\n                errorMessage={errors.lyrics?.message}\n              />\n\n              <Controller\n                control={control}\n                name=\"playlistIds\"\n                render={({ field }) => (\n                  <ChoosePlaylistButtonAndModal\n                    playlistIds={field.value}\n                    setPlaylistIds={field.onChange}\n                  />\n                )}\n              />\n\n              <div className={s.buttonsRow}>\n                <Button variant=\"secondary\" onClick={handleClose} type=\"button\">\n                  {t('tracks.button.cancel')}\n                </Button>\n                <Button variant=\"primary\" type=\"submit\" disabled={!trackId && !isEditMode}>\n                  {isEditMode ? t('tracks.button.save') : t('tracks.button.create')}\n                </Button>\n              </div>\n            </form>\n          </div>\n        )}\n      </DialogContent>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/CreateEditTrackModal/index.ts",
    "content": "export * from './CreateEditTrackModal'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackActions/TrackActions.tsx",
    "content": "import { useMemo, useState } from 'react'\n\nimport { useMeQuery } from '@/features/auth'\nimport { useFetchPlaylistsQuery } from '@/features/playlists'\nimport { ChoosePlaylistModal } from '@/features/playlists/ui/ChoosePlaylistModal/ChoosePlaylistModal'\nimport {\n  TrackActionsMenu,\n  useAddTrackToPlaylistMutation,\n  useDislikeTrackMutation,\n  useLikeTrackMutation,\n  usePublishTrackMutation,\n  useRemoveTrackFromPlaylistMutation,\n  useRemoveTrackMutation,\n  useUnReactionTrackMutation,\n} from '@/features/tracks'\nimport { ReactionButtons, type ReactionButtonsSize } from '@/shared/components'\nimport type { CurrentUserReaction } from '@/shared/types/commonApi.types'\n\nimport { useEditTrackModal } from '../../model/hooks'\nimport { syncTrackPlaylists } from '../../utils/playlistSync'\n\ntype TrackActionsPropsBase = {\n  trackId: string\n  isOwner?: boolean\n  isPublished?: boolean\n  playlistId?: string\n}\n\ntype TrackActionsPropsWithReactions = TrackActionsPropsBase & {\n  reaction: CurrentUserReaction\n  likesCount: number\n  sizeReactionButtons?: ReactionButtonsSize\n}\n\ntype TrackActionsPropsWithoutReactions = TrackActionsPropsBase & {\n  reaction?: undefined\n  likesCount?: undefined\n  sizeReactionButtons?: undefined\n}\n\ntype TrackActionsProps = TrackActionsPropsWithReactions | TrackActionsPropsWithoutReactions\n\nexport const TrackActions = ({\n  reaction,\n  likesCount,\n  trackId,\n  sizeReactionButtons = 'small',\n  isOwner = false,\n  isPublished,\n  playlistId,\n}: TrackActionsProps) => {\n  const [isOpenChoosePlaylistModal, setIsOpenChoosePlaylistModal] = useState(false)\n  const { handleOpenEditTrackModal } = useEditTrackModal()\n\n  const { data: playlists } = useFetchPlaylistsQuery(\n    { trackId },\n    { skip: !isOpenChoosePlaylistModal }\n  )\n\n  // This \"server status\" is the original list of playlists in which the track is located.\n  const originalPlaylistIds = useMemo(\n    () => playlists?.data.map((playlist) => playlist.id) ?? [],\n    [playlists?.data]\n  )\n\n  // This \"UI state\" is what the user selects in the modal window.\n  const [selectedPlaylistIds, setSelectedPlaylistIds] = useState<string[]>([])\n\n  const { data: isAuth } = useMeQuery()\n\n  const [like] = useLikeTrackMutation()\n  const [dislike] = useDislikeTrackMutation()\n  const [unReaction] = useUnReactionTrackMutation()\n\n  const [addTrackToPlaylist] = useAddTrackToPlaylistMutation()\n  const [removeTrackFromPlaylist] = useRemoveTrackFromPlaylistMutation()\n  const [removeTrack] = useRemoveTrackMutation()\n  const [publishTrack] = usePublishTrackMutation()\n\n  const handleOpenChoosePlaylistModal = () => {\n    // When opening the modal window, initialize the selection state with the current state from the server.\n    setSelectedPlaylistIds(originalPlaylistIds)\n    setIsOpenChoosePlaylistModal(true)\n  }\n\n  const handleDelete = () => {\n    if (playlistId) {\n      removeTrackFromPlaylist({ playlistId, trackId })\n    } else {\n      removeTrack({ trackId })\n    }\n  }\n\n  return (\n    <>\n      {reaction !== undefined && (\n        <ReactionButtons\n          reaction={reaction}\n          onLike={() => like({ trackId })}\n          onDislike={() => dislike({ trackId })}\n          likesCount={likesCount}\n          onUnReaction={() => unReaction({ trackId })}\n          size={sizeReactionButtons}\n        />\n      )}\n      {!!isAuth && (\n        <TrackActionsMenu\n          trackId={trackId}\n          isOwner={isOwner}\n          isPublished={isPublished}\n          onEdit={() => handleOpenEditTrackModal(trackId)}\n          onDelete={handleDelete}\n          onAddToPlaylist={handleOpenChoosePlaylistModal}\n          onPublish={() => publishTrack({ trackId })}\n        />\n      )}\n      {isOpenChoosePlaylistModal && (\n        <ChoosePlaylistModal\n          isOpen={isOpenChoosePlaylistModal}\n          setIsOpen={setIsOpenChoosePlaylistModal}\n          playlistIds={selectedPlaylistIds}\n          setPlaylistIds={setSelectedPlaylistIds}\n          onChoose={() => {\n            syncTrackPlaylists({\n              originalPlaylistIds: originalPlaylistIds,\n              newPlaylistIds: selectedPlaylistIds,\n              trackId,\n              addTrackToPlaylist: (params) => addTrackToPlaylist(params).unwrap(),\n              removeTrackFromPlaylist: (params) => removeTrackFromPlaylist(params).unwrap(),\n            })\n          }}\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackActions/TrackActionsMenu/TrackActionsMenu.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { useNavigate } from 'react-router'\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/shared/components'\nimport { Paths } from '@/shared/configs'\nimport { useCurrentPage } from '@/shared/hooks'\nimport {\n  AddToPlaylistIcon,\n  DeleteIcon,\n  EditIcon,\n  MoreIcon,\n  TextIcon,\n  UploadIcon,\n} from '@/shared/icons'\n\ntype TrackActionsMenuProps = {\n  trackId: string\n  isOwner: boolean\n  isPublished?: boolean\n  onEdit: () => void\n  onDelete: () => void\n  onAddToPlaylist: () => void\n  onPublish?: () => void\n}\n\nexport const TrackActionsMenu = ({\n  trackId,\n  isOwner,\n  isPublished,\n  onEdit,\n  onDelete,\n  onAddToPlaylist,\n  onPublish,\n}: TrackActionsMenuProps) => {\n  const { t } = useTranslation()\n  const { isTrackPage, isPlaylistPage } = useCurrentPage()\n  const navigate = useNavigate()\n\n  const showDelete = !isTrackPage\n  const showLyrics = isTrackPage\n\n  const deleteLabel = isPlaylistPage ? 'tracks.button.delete_from_playlist' : 'tracks.button.delete'\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        {isOwner && (\n          <>\n            <DropdownMenuItem onClick={onEdit}>\n              <EditIcon />\n              {t('tracks.button.edit')}\n            </DropdownMenuItem>\n            {!isPublished && onPublish && (\n              <DropdownMenuItem onClick={onPublish}>\n                <UploadIcon width={24} height={24} />\n                {t('tracks.button.publish')}\n              </DropdownMenuItem>\n            )}\n            {showDelete && (\n              <DropdownMenuItem onClick={onDelete}>\n                <DeleteIcon width={24} height={24} />\n                {t(deleteLabel)}\n              </DropdownMenuItem>\n            )}\n          </>\n        )}\n        <DropdownMenuItem onClick={onAddToPlaylist}>\n          <AddToPlaylistIcon />\n          {t('tracks.button.add_to_playlist')}\n        </DropdownMenuItem>\n        {showLyrics && (\n          <DropdownMenuItem onClick={() => navigate(`${Paths.TracksLyrics}/${trackId}`)}>\n            <TextIcon />\n            {t('tracks.button.show_text_song')}\n          </DropdownMenuItem>\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackActions/TrackActionsMenu/index.ts",
    "content": "export * from './TrackActionsMenu'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackActions/index.ts",
    "content": "export * from './TrackActions'\nexport * from './TrackActionsMenu'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackCard/TrackCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n\n  width: 128px;\n\n  transition: background-color 0.2s;\n}\n\n.image {\n  position: relative;\n  overflow: hidden;\n  height: 103px;\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:has(> .image:hover) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.playback {\n  position: absolute;\n  z-index: 999;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n\n  width: 50%;\n  height: 50%;\n\n  opacity: 0;\n\n  transition: opacity 0.2s;\n}\n\n.image:hover .playback {\n  opacity: 1;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.artists {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  text-overflow: ellipsis;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackCard/TrackCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_TRACKS } from '@/features/tracks'\nimport { ImageType } from '@/shared/types'\n\nimport { TrackCard } from './TrackCard'\n\nconst meta: Meta<typeof TrackCard> = {\n  title: 'entities/TrackCard',\n  component: TrackCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TrackCard>\n\nexport const Default: Story = {\n  args: {\n    track: {\n      ...MOCK_TRACKS[0],\n      type: 'tracks',\n      attributes: {\n        ...MOCK_TRACKS[0].attributes,\n        images: {\n          main: [\n            {\n              ...MOCK_TRACKS[0].attributes.images.main[0],\n              type: 'original' as ImageType,\n            },\n          ],\n        },\n      },\n      relationships: {\n        artists: {\n          data: [],\n        },\n      },\n    },\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    track: {\n      ...MOCK_TRACKS[2],\n      type: 'tracks',\n      attributes: {\n        ...MOCK_TRACKS[2].attributes,\n        title: 'A very long track title that should be truncated',\n        images: {\n          main: [\n            {\n              ...MOCK_TRACKS[2].attributes.images.main[0],\n              type: 'original' as ImageType,\n            },\n          ],\n        },\n      },\n      relationships: {\n        artists: {\n          data: [],\n        },\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackCard/TrackCard.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { Link } from 'react-router'\n\nimport {\n  type FetchTracksAttributes,\n  type TrackDetails,\n  useDislikeTrackMutation,\n  useLikeTrackMutation,\n  useUnReactionTrackMutation,\n} from '@/features/tracks'\nimport { useCurrentTrack, usePlaybackState, usePlayerControls } from '@/player'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { Card, IconButton, ReactionButtons, Typography } from '@/shared/components'\nimport { PauseIcon, PlayIcon } from '@/shared/icons'\nimport { ImageType } from '@/shared/types'\nimport { getImageByType } from '@/shared/utils'\n\nimport s from './TrackCard.module.css'\n\ntype Props = {\n  track: TrackDetails<FetchTracksAttributes>\n  artists: string\n  handleTrackCardPlaybackClick: (trackId: string) => void\n}\n\nexport const TrackCard = ({ track, artists, handleTrackCardPlaybackClick }: Props) => {\n  const { t } = useTranslation()\n  const [like] = useLikeTrackMutation({\n    fixedCacheKey: `track-reaction-${track.id}`,\n  })\n  const [dislike] = useDislikeTrackMutation({\n    fixedCacheKey: `track-reaction-${track.id}`,\n  })\n  const [unReaction] = useUnReactionTrackMutation({\n    fixedCacheKey: `track-reaction-${track.id}`,\n  })\n\n  const { trackId: playerTrackId } = useCurrentTrack()\n  const { pause, resume } = usePlayerControls()\n  const { isPlaying } = usePlaybackState()\n\n  const isPlayerTrack = playerTrackId && playerTrackId === track.id\n  const isTrackPlaying = isPlayerTrack && isPlaying\n\n  const trackCover =\n    getImageByType(track.attributes.images, ImageType.MEDIUM)?.url || noCoverPlaceholder\n\n  const handlePlayback = () => {\n    if (isPlayerTrack) {\n      if (isPlaying) {\n        pause()\n      } else {\n        resume()\n      }\n      return\n    }\n    handleTrackCardPlaybackClick(track.id)\n  }\n\n  return (\n    <Card className={s.card}>\n      <div className={s.image}>\n        <img src={trackCover} alt={track.attributes.title} />\n        <IconButton className={s.playback} onClick={handlePlayback}>\n          {isTrackPlaying ? <PauseIcon /> : <PlayIcon />}\n        </IconButton>\n      </div>\n\n      <Typography variant=\"h3\" className={s.title} as={Link} to={`/tracks/${track.id}`}>\n        {track.attributes.title}\n      </Typography>\n\n      <Typography variant=\"body3\" className={s.artists}>\n        {artists || t('player.unknown_artist')}\n      </Typography>\n      <ReactionButtons\n        reaction={track.attributes.currentUserReaction}\n        onLike={() => like({ trackId: track.id })}\n        onDislike={() => dislike({ trackId: track.id })}\n        likesCount={track.attributes.likesCount}\n        onUnReaction={() => unReaction({ trackId: track.id })}\n      />\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackCard/index.ts",
    "content": "export * from './TrackCard'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackInfoCell/TrackInfoCell.module.css",
    "content": ".box {\n  display: flex;\n  gap: 21px;\n}\n\n.image {\n  position: relative;\n  flex-shrink: 0;\n  width: 52px;\n  height: 52px;\n  cursor: pointer;\n  transition:\n    transform 0.25s ease,\n    box-shadow 0.25s ease;\n}\n\n.image img {\n  object-fit: cover;\n  width: 100%;\n  height: 100%;\n  display: block;\n}\n\n.info {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n\n  max-width: 280px;\n  min-width: 0;\n}\n\n.titleRow {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  min-width: 0;\n}\n\n.draftBadge {\n  flex-shrink: 0;\n  font-size: 11px;\n  line-height: 1;\n  padding: 2px 6px;\n  border-radius: 4px;\n  background-color: var(--color-text-secondary);\n  color: var(--color-bg-primary);\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  font-weight: 600;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.title.playing {\n  color: var(--color-accent);\n}\n\n.artists {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.image:hover {\n  transform: scale(0.95);\n  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);\n}\n\n.playButton {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  background-color: var(--color-bg-card);\n  border: none;\n  padding: 0;\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 0.15s ease;\n}\n\n.boxHovered .playButton {\n  opacity: 1;\n  pointer-events: auto;\n}\n\n.playButton svg {\n  width: var(--font-size-xxl);\n  height: var(--font-size-xxl);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackInfoCell/TrackInfoCell.tsx",
    "content": "import clsx from 'clsx'\nimport { Link } from 'react-router'\n\nimport { useTranslation } from 'react-i18next'\n\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { IconButton, TableCell, Typography } from '@/shared/components'\nimport { PauseIcon, PlayIcon } from '@/shared/icons'\n\nimport s from './TrackInfoCell.module.css'\n\ntype TrackInfoCellProps = {\n  imageSrc?: string\n  isHovered: boolean\n  title: string\n  artists: string[]\n  isPlaying: boolean\n  isPublished?: boolean\n  id: string\n  onTrackPlayClick?: (trackId: string) => void\n}\n\nexport const TrackInfoCell = ({\n  imageSrc = noCoverPlaceholder,\n  title,\n  artists,\n  isHovered,\n  isPlaying,\n  isPublished,\n  id,\n  onTrackPlayClick,\n}: TrackInfoCellProps) => {\n  const { t } = useTranslation()\n\n  return (\n    <TableCell>\n      <div className={clsx(s.box, { [s.boxHovered]: isHovered })}>\n        <div className={s.image}>\n          <img src={imageSrc} alt={title} />\n          <IconButton\n            aria-label=\"Play track\"\n            className={s.playButton}\n            type=\"button\"\n            onClick={() => onTrackPlayClick?.(id)}>\n            {isPlaying ? <PauseIcon /> : <PlayIcon />}\n          </IconButton>\n        </div>\n        <div className={s.info}>\n          <div className={s.titleRow}>\n            <Typography\n              variant=\"body1\"\n              as={Link}\n              className={clsx(s.title, isPlaying && s.playing)}\n              to={`/tracks/${id}`}>\n              {title}\n            </Typography>\n            {isPublished === false && (\n              <span className={s.draftBadge}>{t('tracks.button.draft')}</span>\n            )}\n          </div>\n          <Typography className={s.artists} variant=\"body2\">\n            {artists.length > 0 ? artists.join(', ') : t('player.unknown_artist')}\n          </Typography>\n        </div>\n      </div>\n    </TableCell>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackInfoCell/index.ts",
    "content": "export * from './TrackInfoCell.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackOverview/TrackOverview.module.css",
    "content": ".container {\n  display: flex;\n  gap: 24px;\n  background: transparent;\n}\n\n.imageContainer {\n  flex-shrink: 0;\n  width: 297px;\n  height: 297px;\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  box-shadow: 0 4px 60px rgba(0, 0, 0, 0.5);\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  min-width: 0;\n}\n\n.title {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  margin-bottom: 8px;\n\n  font-size: clamp(var(--font-size-xxl), 8vw, var(--font-size-xxxl));\n  font-weight: 900;\n  line-height: 1;\n  white-space: pre-wrap;\n}\n\n.description {\n  opacity: 0.7;\n}\n\n.info {\n  margin-top: auto;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackOverview/TrackOverview.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_5_HASHTAGS } from '@/features/tags'\n\nimport { TrackOverview } from './TrackOverview'\n\nconst meta: Meta<typeof TrackOverview> = {\n  title: 'entities/TrackOverview',\n  component: TrackOverview,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TrackOverview>\n\nexport const Default: Story = {\n  args: {\n    title: 'Chill Mix',\n    image: 'https://unsplash.it/297/297',\n    artists: ['Julia Wolf', 'ayokay', 'Khalid'],\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n\nexport const LongTitle: Story = {\n  args: {\n    title: 'This is a Very Long Track Title That Should Scale Responsively',\n    image: 'https://unsplash.it/299/299',\n    artists: ['Julia Wolf', 'ayokay', 'Khalid'],\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackOverview/TrackOverview.tsx",
    "content": "import clsx from 'clsx'\nimport { type ComponentProps } from 'react'\n\nimport { TagsList } from '@/features/tags'\nimport type { Tag } from '@/features/tags/api/tagsApi.types'\nimport Placeholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { Typography } from '@/shared/components'\n\nimport s from './TrackOverview.module.css'\nimport { useTranslation } from 'react-i18next'\n\ntype TrackOverviewProps = {\n  title: string\n  image?: string\n  addedAt: string\n  artists: string[]\n  tags: Tag[]\n} & ComponentProps<'div'>\n\nexport const TrackOverview = ({\n  title,\n  image = Placeholder,\n  addedAt,\n  tags,\n  className,\n  artists,\n  ...props\n}: TrackOverviewProps) => {\n  const { t } = useTranslation()\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <div className={s.imageContainer}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <div className={s.content}>\n        <TagsList tags={tags} entity=\"tracks\" />\n\n        <Typography variant=\"h1\" as=\"h1\" className={s.title}>\n          {title}\n        </Typography>\n\n        <div className={s.info}>\n          <Typography variant=\"body1\">{artists.join(', ')}</Typography>\n          <Typography variant=\"body2\">\n            {' '}\n            {`${t('tracks.release')} ${new Date(addedAt).toLocaleDateString()}`}\n          </Typography>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackOverview/index.ts",
    "content": "export * from './TrackOverview'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackRow/TrackRow.module.css",
    "content": ".active {\n  background-color: var(--color-bg-interactive-secondary);\n}\n\n.draft {\n  opacity: 0.6;\n}\n\n.playing {\n  color: var(--color-accent);\n}\n\n.progress {\n  width: 183px;\n}\n\n.actions {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackRow/TrackRow.tsx",
    "content": "import clsx from 'clsx'\nimport type { ReactNode } from 'react'\n\nimport type { TrackRowData } from '@/features/tracks'\nimport { useCurrentTrack, usePlaybackProgress } from '@/player'\nimport { Progress, TableCell, TableRow, Typography } from '@/shared/components'\nimport { useHover } from '@/shared/hooks'\nimport { LiveWaveIcon, StaticWaveIcon } from '@/shared/icons'\n\nimport { TrackInfoCell } from '../TrackInfoCell'\nimport s from './TrackRow.module.css'\n\ntype TrackRowProps<T> = {\n  renderActionsCell: (trackRow: T) => ReactNode\n  trackRow: T\n  onTrackPlayClick?: (trackId: string) => void\n}\n\nexport const TrackRow = <T extends TrackRowData>({\n  trackRow,\n  renderActionsCell,\n  onTrackPlayClick,\n}: TrackRowProps<T>) => {\n  const [ref, isHovered] = useHover<HTMLTableRowElement>()\n\n  const { trackId, isPlaying } = useCurrentTrack()\n\n  const { progress } = usePlaybackProgress()\n  const isPlayerTrack = trackRow.id === trackId\n\n  const isTrackRowPlaying = isPlayerTrack && isPlaying\n  const isTrackSelected = isPlayerTrack && !isPlaying\n\n  const getTableCellIcon = () => {\n    if (isTrackRowPlaying) return <LiveWaveIcon />\n    if (isTrackSelected) return <StaticWaveIcon />\n\n    return trackRow.index + 1\n  }\n\n  return (\n    <TableRow\n      ref={ref}\n      className={clsx({\n        [s.active]: isTrackRowPlaying,\n        [s.draft]: trackRow.isPublished === false,\n      })}>\n      <TableCell className={clsx(isPlayerTrack && s.playing)}>{getTableCellIcon()}</TableCell>\n      <TrackInfoCell\n        id={trackRow.id}\n        isHovered={isHovered}\n        imageSrc={trackRow.imageSrc}\n        title={trackRow.title}\n        artists={trackRow.artists}\n        isPlaying={isTrackRowPlaying}\n        isPublished={trackRow.isPublished}\n        onTrackPlayClick={onTrackPlayClick}\n      />\n      <TableCell>\n        {isTrackRowPlaying && (\n          <Progress className={s.progress} value={progress ?? 0} max={trackRow.duration} />\n        )}\n      </TableCell>\n      <TableCell>\n        <Typography variant=\"body2\" as=\"time\" dateTime={trackRow.addedAt}>\n          {new Date(trackRow.addedAt).toLocaleDateString()}\n        </Typography>\n      </TableCell>\n      <TableCell>\n        <div className={s.actions}>{renderActionsCell(trackRow)}</div>\n      </TableCell>\n      <TableCell>\n        <Typography variant=\"body2\">{trackRow.duration}</Typography>\n      </TableCell>\n    </TableRow>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackRowContainer/TrackRowContainer.tsx",
    "content": "import { useFetchTrackByIdQuery } from '@/features/tracks/api'\nimport { TrackActions } from '@/features/tracks/ui/TrackActions'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport type { TrackRowData } from '@/features/tracks/ui/TracksTable'\nimport { CurrentUserReaction } from '@/shared/types'\n\ntype TrackRowContainerProps = {\n  trackRow: TrackRowData\n  playlistId?: string\n  userId?: string\n}\n\nexport const TrackRowContainer = ({ trackRow, userId, playlistId }: TrackRowContainerProps) => {\n  const { data: trackData } = useFetchTrackByIdQuery({ trackId: trackRow.id })\n  const isTrackOwner = userId === trackData?.data.attributes.user.id\n\n  const trackRowWithPublished = {\n    ...trackRow,\n    isPublished: trackData?.data.attributes.isPublished,\n  }\n\n  return (\n    <TrackRow\n      trackRow={trackRowWithPublished}\n      renderActionsCell={(row) => (\n        <TrackActions\n          likesCount={row.likesCount ?? 0}\n          reaction={row.currentUserReaction ?? CurrentUserReaction.None}\n          trackId={row.id}\n          isOwner={isTrackOwner}\n          isPublished={row.isPublished}\n          playlistId={playlistId}\n        />\n      )}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TrackRowContainer/index.ts",
    "content": "export * from './TrackRowContainer'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TracksTable/TrackTable.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport {\n  CurrentUserReaction,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  ReactionButtons,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport { MOCK_TRACKS } from '../../api'\nimport { TracksTable } from './TracksTable'\n\nconst meta: Meta<typeof TracksTable> = {\n  title: 'entities/TracksTable',\n  component: TracksTable,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TracksTable>\n\ntype ReactionsProps =\n  | {\n      likesCount: number\n      dislikesCount: number\n      currentUserReaction: CurrentUserReaction\n    }\n  | {\n      likesCount?: undefined\n      dislikesCount?: undefined\n      currentUserReaction?: undefined\n    }\n\nexport type TrackRowData = {\n  index: number\n  image: string\n  id: string\n  title: string\n  addedAt: string\n  artists: string[]\n  duration: number\n} & ReactionsProps\n\n// FIXME: temporary build fix, need to add url\nexport const Default: Story = {\n  args: {\n    trackRows: MOCK_TRACKS.map((track, index) => ({\n      index: index,\n      id: track.id,\n      title: track.attributes.title,\n      image: track.attributes.images.main[0].url,\n      addedAt: track.attributes.addedAt,\n      artists: track.attributes.artists?.map((artist) => artist.name) || [],\n      isPlaying: false,\n      likesCount: track.attributes.likesCount,\n      dislikesCount: track.attributes.dislikesCount,\n      currentUserReaction: track.attributes.currentUserReaction,\n      duration: track.attributes.duration,\n      url: '',\n    })),\n    renderTrackRow: (trackRow) => (\n      <TrackRow\n        trackRow={trackRow}\n        renderActionsCell={() => (\n          <>\n            <ReactionButtons\n              onUnReaction={() => {}}\n              reaction={trackRow.currentUserReaction}\n              onLike={() => {}}\n              onDislike={() => {}}\n              likesCount={trackRow.likesCount}\n            />\n\n            <DropdownMenu>\n              <DropdownMenuTrigger>\n                <MoreIcon />\n              </DropdownMenuTrigger>\n\n              <DropdownMenuContent>\n                <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                  Add to playlist\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                  Show text song\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </>\n        )}\n      />\n    ),\n  },\n}\n\n// FIXME: temporary build fix, need to add url\nexport const WithoutReactions: Story = {\n  args: {\n    trackRows: MOCK_TRACKS.map((track, index) => ({\n      index: index,\n      id: track.id,\n      title: track.attributes.title,\n      image: track.attributes.images.main[0].url,\n      addedAt: track.attributes.addedAt,\n      artists: track.attributes.artists?.map((artist) => artist.name) || [],\n      duration: track.attributes.duration,\n      url: '',\n    })),\n    renderTrackRow: (trackRow) => (\n      <TrackRow\n        trackRow={trackRow}\n        renderActionsCell={() => (\n          <div>\n            <DropdownMenu>\n              <DropdownMenuTrigger>\n                <MoreIcon />\n              </DropdownMenuTrigger>\n\n              <DropdownMenuContent>\n                <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                  Add to playlist\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                  Show text song\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        )}\n      />\n    ),\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TracksTable/TracksTable.tsx",
    "content": "import { t } from 'i18next'\nimport type { ReactNode } from 'react'\n\nimport {\n  CurrentUserReaction,\n  Table,\n  TableBody,\n  TableHead,\n  TableHeaderCell,\n  TableRow,\n} from '@/shared/components'\nimport { ClockIcon } from '@/shared/icons'\n\ntype TableColumn = {\n  title: ReactNode\n  width?: string\n}\n\nexport type TracksTableProps<T extends TrackRowData> = {\n  trackRows: T[]\n  renderTrackRow: (trackRow: T) => ReactNode\n}\n\ntype ReactionsProps =\n  | {\n      likesCount: number\n      dislikesCount: number\n      currentUserReaction: CurrentUserReaction\n    }\n  | {\n      likesCount?: undefined\n      dislikesCount?: undefined\n      currentUserReaction?: undefined\n    }\n\nexport type TrackRowData = {\n  index: number\n  imageSrc?: string\n  id: string\n  title: string\n  addedAt: string\n  artists: string[]\n  duration: number\n  isOwner?: boolean\n  isPublished?: boolean\n  url: string\n} & ReactionsProps\n\nconst TABLE_COLUMNS: TableColumn[] = [\n  { title: '#', width: '40px' },\n  { title: t('tracks.table.track') },\n  { title: '' },\n  { title: t('tracks.table.date_added'), width: '120px' },\n  { title: t('tracks.table.actions'), width: '150px' },\n  { title: <ClockIcon />, width: '60px' },\n]\n\nexport const TracksTable = <T extends TrackRowData>({\n  trackRows = [],\n  renderTrackRow,\n}: TracksTableProps<T>) => {\n  if (trackRows.length === 0) {\n    return <div>{t('tracks.label.no_tracks')}</div>\n  }\n\n  return (\n    <Table>\n      <TableHead>\n        <TableRow>\n          {TABLE_COLUMNS.map((column, index) => (\n            <TableHeaderCell key={index} style={{ width: column.width }}>\n              {column.title}\n            </TableHeaderCell>\n          ))}\n        </TableRow>\n      </TableHead>\n      <TableBody>{trackRows.map((trackRow) => renderTrackRow(trackRow))}</TableBody>\n    </Table>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TracksTable/index.ts",
    "content": "export * from './TracksTable'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TracksTableSkeleton/TracksTableSkeleton.tsx",
    "content": "import { Skeleton, Table, TableHead, TableHeaderCell, TableRow } from '@/shared/components'\nimport { ClockIcon } from '@/shared/icons'\nimport { useTranslation } from 'react-i18next'\n\ntype Props = {\n  count?: number\n}\n\nexport const TracksTableSkeleton = ({ count = 5 }: Props) => {\n  const { t } = useTranslation()\n\n  const TABLE_COLUMNS = [\n    { title: '#', width: '40px' },\n    { title: t('tracks.table.track') },\n    { title: '' },\n    { title: t('tracks.table.date_added'), width: '120px' },\n    { title: t('tracks.table.actions'), width: '150px' },\n    { title: <ClockIcon />, width: '60px' },\n  ]\n\n  return (\n    <>\n      <Table>\n        <TableHead>\n          <TableRow>\n            {TABLE_COLUMNS.map((column, index) => (\n              <TableHeaderCell key={index} style={{ width: column.width }}>\n                {column.title}\n              </TableHeaderCell>\n            ))}\n          </TableRow>\n        </TableHead>\n      </Table>\n      {Array.from({ length: count }).map((_el, i) => (\n        <Skeleton height={'70px'} key={i} />\n      ))}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/TracksTableSkeleton/index.ts",
    "content": "export * from './TracksTableSkeleton.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/ui/index.ts",
    "content": "export * from './CreateEditTrackModal'\nexport * from './TrackActions'\nexport * from './TrackCard'\nexport * from './TrackOverview'\nexport * from './TrackRow/TrackRow'\nexport * from './TrackRowContainer'\nexport * from './TracksTable'\nexport * from './TracksTableSkeleton'\n"
  },
  {
    "path": "apps/rtk-query/src/features/tracks/utils/playlistSync.ts",
    "content": "type PlaylistSyncParams = {\n  originalPlaylistIds: string[]\n  newPlaylistIds: string[]\n  trackId: string\n  addTrackToPlaylist: (params: { trackId: string; playlistId: string }) => Promise<unknown>\n  removeTrackFromPlaylist: (params: { trackId: string; playlistId: string }) => Promise<unknown>\n}\n\n/**\n * Sync track playlists\n * - Add track to new playlists\n * - Remove track from playlists where it is no longer present\n */\nexport const syncTrackPlaylists = async ({\n  originalPlaylistIds,\n  newPlaylistIds,\n  trackId,\n  addTrackToPlaylist,\n  removeTrackFromPlaylist,\n}: PlaylistSyncParams): Promise<void> => {\n  const promises: Promise<unknown>[] = []\n\n  // Add track to new playlists\n  const playlistsToAdd = newPlaylistIds.filter(\n    (playlistId) => !originalPlaylistIds.includes(playlistId)\n  )\n  for (const playlistId of playlistsToAdd) {\n    promises.push(addTrackToPlaylist({ trackId, playlistId }))\n  }\n\n  // Remove track from playlists where it is no longer present\n  const playlistsToRemove = originalPlaylistIds.filter(\n    (playlistId) => !newPlaylistIds.includes(playlistId)\n  )\n  for (const playlistId of playlistsToRemove) {\n    promises.push(removeTrackFromPlaylist({ trackId, playlistId }))\n  }\n\n  await Promise.all(promises)\n}\n\n/**\n * Add track to all specified playlists\n */\nexport const addTrackToPlaylists = async (\n  trackId: string,\n  playlistIds: string[],\n  addTrackToPlaylist: (params: { trackId: string; playlistId: string }) => Promise<unknown>\n): Promise<void> => {\n  const promises = playlistIds.map((playlistId) => addTrackToPlaylist({ trackId, playlistId }))\n  await Promise.all(promises)\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/AppLoader/AppLoader.tsx",
    "content": "import { Loader } from '@/shared/components'\nimport { useIsGlobalLoading } from '@/shared/hooks'\n\nexport const AppLoader = () => {\n  const isLoading = useIsGlobalLoading()\n  if (!isLoading) return null\n  return <Loader />\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/AppLoader/index.ts",
    "content": "export { AppLoader } from './AppLoader'\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Header/AccountMenu/AccountMenu.module.css",
    "content": ".trigger {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  gap: 11px;\n  align-items: center;\n\n  padding: 3px 39px 3px 3px;\n  border-radius: 40px;\n}\n\n.trigger::after {\n  content: '';\n\n  position: absolute;\n  top: 50%;\n  right: 12px;\n  transform: translateY(-50%);\n\n  display: block;\n\n  width: 14px;\n  height: 7px;\n\n  background-color: var(--color-text-primary);\n  clip-path: polygon(50% 100%, 0 0, 100% 0);\n\n  transition: clip-path 0.3s ease;\n}\n\n.trigger[data-open]::after {\n  clip-path: polygon(50% 0, 0 100%, 100% 100%);\n}\n\n.avatar {\n  overflow: hidden;\n\n  width: 34px;\n  height: 34px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-xxxs);\n}\n\n.name {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Header/AccountMenu/AccountMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { AccountMenu } from './AccountMenu'\n\nconst meta: Meta<typeof AccountMenu> = {\n  title: 'entities/AccountMenu',\n  component: AccountMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof AccountMenu>\n\nexport const Default: Story = {\n  args: {\n    avatar: 'https://unsplash.it/182/182',\n    fullName: { name: 'Kanye', surname: 'West' },\n    id: '1',\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Header/AccountMenu/AccountMenu.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { Link } from 'react-router'\n\nimport { useLogoutMutation } from '@/features/auth'\nimport type { FullName } from '@/features/profile'\nimport {\n  Avatar,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Typography,\n} from '@/shared/components'\nimport { Paths } from '@/shared/configs'\nimport { LogoutIcon, ProfileIcon } from '@/shared/icons'\n\nimport s from './AccountMenu.module.css'\n\ntype AccountMenuProps = {\n  avatar: string | null\n  fullName: FullName\n  userLogin: string\n  id: string\n}\n\nexport const AccountMenu = ({ avatar, fullName, userLogin, id }: AccountMenuProps) => {\n  const { t } = useTranslation()\n\n  const [logout] = useLogoutMutation()\n\n  const profileName = fullName?.name ? `${fullName.name} ${fullName.surname}` : userLogin\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild className={s.trigger}>\n        <Avatar className={s.avatar} src={avatar} fullName={fullName} userLogin={userLogin} />\n\n        <Typography className={s.name} variant=\"body2\">\n          {profileName}\n        </Typography>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem as={Link} to={`${Paths.Profile}/${id}`}>\n          <ProfileIcon />\n          <span>{t('auth.title.my_profile')}</span>\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => logout()}>\n          <LogoutIcon />\n          <span>{t('auth.title.logout')}</span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Header/AccountMenu/index.ts",
    "content": "export * from './AccountMenu'\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Header/Header.module.css",
    "content": ".header {\n  position: fixed;\n  z-index: 1;\n  right: 0;\n  left: 0;\n\n  display: flex;\n  grid-area: header;\n  align-items: center;\n  justify-content: space-between;\n\n  height: var(--header-height);\n  padding: 0 32px;\n}\n\n.actions {\n  min-width: 135px;\n  height: 40px;\n  display: flex;\n  column-gap: 20px;\n  align-items: center;\n\n  padding-left: 5px;\n  border-radius: 40px;\n\n  background-color: rgb(0 0 0 / 80%);\n  backdrop-filter: blur(2px);\n\n  transition: 0.2s;\n}\n\n.actions:hover {\n  background-color: var(--color-bg-primary);\n}\n\n.actionsSkeleton {\n  width: 100%;\n  height: 100%;\n  background-color: transparent;\n  border-radius: 40px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Header/Header.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { useLocation } from 'react-router'\n\nimport { useMeQuery } from '@/features/auth/api'\nimport { setIsAuthModalOpen } from '@/features/auth/model'\nimport { selectProfileAvatar, selectProfileFullName } from '@/features/profile'\nimport { AccountMenu } from '@/layout/Header/AccountMenu'\nimport {\n  Button,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Skeleton,\n} from '@/shared/components'\nimport { Paths } from '@/shared/configs'\nimport { useAppDispatch, useAppSelector } from '@/shared/hooks'\nimport { LanguageIcon } from '@/shared/icons/LanguageIcon.tsx'\nimport { setLocale } from '@/shared/utils'\n\nimport s from './Header.module.css'\n\nexport const Header = () => {\n  const { t } = useTranslation()\n\n  const { data: user, isLoading } = useMeQuery()\n  const dispatch = useAppDispatch()\n  const isAuth = !!user\n  const profileAvatarUrl = useAppSelector(selectProfileAvatar)\n  const profileFullName = useAppSelector(selectProfileFullName)\n\n  const location = useLocation()\n  const hasColorBg = ([Paths.Playlists, Paths.Tracks, Paths.Main] as string[]).includes(\n    location.pathname\n  )\n\n  const renderActions = () => {\n    if (isLoading) {\n      return <Skeleton className={s.actionsSkeleton} />\n    }\n\n    if (isAuth) {\n      return (\n        <AccountMenu\n          avatar={profileAvatarUrl}\n          fullName={profileFullName}\n          userLogin={user.login}\n          id={user.userId}\n        />\n      )\n    }\n\n    return (\n      <Button onClick={() => dispatch(setIsAuthModalOpen({ isAuthModalOpen: true }))}>\n        {t('auth.button.sign_in')}\n      </Button>\n    )\n  }\n\n  return (\n    <header\n      className={s.header}\n      style={{ backgroundColor: hasColorBg ? 'var(--color-bg-primary)' : '' }}>\n      <div className={s.logo}>Musicfun</div>\n      <div className={s.actions}>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <LanguageIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent>\n            <DropdownMenuItem onClick={() => setLocale('en')}>English</DropdownMenuItem>\n            <DropdownMenuItem onClick={() => setLocale('ru')}>Русский</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n\n        {renderActions()}\n      </div>\n    </header>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Header/index.ts",
    "content": "export * from './AccountMenu'\nexport { Header } from './Header'\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Layout.module.css",
    "content": ".grid {\n  display: grid;\n  grid-template: 'sidebar main' 1fr / 310px 1fr;\n  height: 100vh;\n}\n\n.grid.playerOpen {\n  grid-template: 'sidebar main' 1fr 'player player' var(--player-height) / 310px 1fr;\n}\n\n.main {\n  overflow-y: auto;\n  grid-area: main;\n  padding-top: var(--header-height);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Layout.tsx",
    "content": "import clsx from 'clsx'\nimport { Outlet } from 'react-router'\n\nimport { LoginModal } from '@/features/auth'\nimport { selectIsAuthModalOpen } from '@/features/auth/model'\nimport { CreateEditPlaylistModal, selectIsCreateEditModalOpen } from '@/features/playlists'\nimport { selectIsEditProfileModalOpen } from '@/features/profile'\nimport { EditProfileModal } from '@/features/profile'\nimport { CreateEditTrackModal, selectIsCreateEditTrackModalOpen } from '@/features/tracks'\nimport { AppLoader } from '@/layout/AppLoader'\nimport { useAppSelector } from '@/shared/hooks'\nimport { Player } from '@/widgets/Player'\n\nimport { Header } from './Header'\nimport s from './Layout.module.css'\nimport { Sidebar } from './Sidebar'\n\nexport const Layout = () => {\n  const IS_PLAYER_OPEN = true\n  const isCreatePlaylistModalOpen = useAppSelector(selectIsCreateEditModalOpen)\n  const isCreateTrackModalOpen = useAppSelector(selectIsCreateEditTrackModalOpen)\n  const isAuthModalOpen = useAppSelector(selectIsAuthModalOpen)\n  const isEditProfileOpen = useAppSelector(selectIsEditProfileModalOpen)\n\n  return (\n    <>\n      <AppLoader />\n      <div className={clsx(s.grid, IS_PLAYER_OPEN && s.playerOpen)}>\n        <Header />\n\n        <Sidebar />\n        <main className={s.main}>\n          <Outlet />\n        </main>\n        {IS_PLAYER_OPEN && <Player />}\n        {isAuthModalOpen && <LoginModal />}\n        {isCreatePlaylistModalOpen && <CreateEditPlaylistModal />}\n        {isCreateTrackModalOpen && <CreateEditTrackModal />}\n        {isEditProfileOpen && <EditProfileModal />}\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Sidebar/MenuLinks/MenuLinks.module.css",
    "content": ".column {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list + .list {\n  padding-top: 20px;\n  border-top: 1px solid var(--color-bg-secondary);\n}\n\n.link {\n  all: unset;\n\n  cursor: pointer;\n\n  display: flex;\n  gap: 16px;\n  align-items: center;\n\n  width: fit-content;\n\n  font-size: var(--font-size-m);\n  font-weight: 700;\n  color: var(--color-text-secondary);\n\n  transition: color 0.2s ease;\n}\n\n.link:hover {\n  color: var(--color-text-primary);\n}\n\n.active {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Sidebar/MenuLinks/MenuLinks.tsx",
    "content": "import clsx from 'clsx'\nimport { useTranslation } from 'react-i18next'\nimport { NavLink } from 'react-router'\n\nimport { useMeQuery } from '@/features/auth'\nimport { setIsAuthModalOpen } from '@/features/auth/model'\nimport { useCreatePlaylistModal } from '@/features/playlists'\nimport { useCreateTrackModal } from '@/features/tracks'\nimport { Paths } from '@/shared/configs'\nimport { useAppDispatch } from '@/shared/hooks'\nimport { HomeIcon, LibraryIcon, PlaylistIcon, TrackIcon, UploadIcon } from '@/shared/icons'\nimport { CreateIcon } from '@/shared/icons/CreateIcon'\n\nimport s from './MenuLinks.module.css'\n\ntype MenuLink = {\n  to: string\n  icon: React.ReactNode\n  label: string\n}\n\ntype MenuButton = {\n  onClick: () => void\n  icon: React.ReactNode\n  label: string\n}\n\nexport const MenuLinks = () => {\n  const { t } = useTranslation()\n\n  const { data: user } = useMeQuery()\n  const { handleOpenCreatePlaylistModal } = useCreatePlaylistModal()\n  const { handleOpenCreateTrackModal } = useCreateTrackModal()\n  const dispatch = useAppDispatch()\n  const handleOpenAuthModal = () => {\n    dispatch(setIsAuthModalOpen({ isAuthModalOpen: true }))\n  }\n\n  const createLinks: MenuLink[] = [\n    {\n      to: Paths.Tracks,\n      icon: <TrackIcon />,\n      label: t('sidebar.all_tracks'),\n    },\n    {\n      to: Paths.Playlists,\n      icon: <PlaylistIcon />,\n      label: t('sidebar.all_playlists'),\n    },\n  ]\n\n  const actionButtons: MenuButton[] = [\n    {\n      onClick: user ? handleOpenCreateTrackModal : handleOpenAuthModal,\n      icon: <UploadIcon />,\n      label: t('sidebar.upload_track'),\n    },\n    {\n      onClick: user ? handleOpenCreatePlaylistModal : handleOpenAuthModal,\n      icon: <CreateIcon />,\n      label: t('sidebar.create_playlist'),\n    },\n  ]\n\n  return (\n    <nav className={s.column} aria-label=\"Main navigation\">\n      <ul className={s.list}>\n        <li>\n          <SidebarLink\n            to={Paths.Main}\n            icon={<HomeIcon width={32} height={32} />}\n            label={t('sidebar.home')}\n          />\n        </li>\n        {user ? (\n          <li>\n            <SidebarLink\n              to={`${Paths.Profile}/${user?.userId}`}\n              icon={<LibraryIcon />}\n              label={t('sidebar.your_library')}\n            />\n          </li>\n        ) : (\n          <li>\n            <SidebarButton\n              onClick={handleOpenAuthModal}\n              icon={<LibraryIcon />}\n              label={t('sidebar.your_library')}\n            />\n          </li>\n        )}\n      </ul>\n      <ul className={s.list}>\n        {actionButtons.map((props) => (\n          <li key={props.label}>\n            <SidebarButton {...props} />\n          </li>\n        ))}\n      </ul>\n      <ul className={s.list}>\n        {createLinks.map((props) => (\n          <li key={props.to}>\n            <SidebarLink {...props} />\n          </li>\n        ))}\n      </ul>\n    </nav>\n  )\n}\n\nconst SidebarLink = ({ to, icon, label }: MenuLink) => (\n  <NavLink to={to} className={({ isActive }) => clsx(s.link, isActive && s.active)}>\n    {icon}\n    {label}\n  </NavLink>\n)\n\nconst SidebarButton = ({ onClick, icon, label }: MenuButton) => (\n  <button onClick={onClick} className={s.link} type=\"button\">\n    {icon}\n    {label}\n  </button>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Sidebar/MenuLinks/index.ts",
    "content": "export * from './MenuLinks'\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Sidebar/Sidebar.module.css",
    "content": ".sidebar {\n  overflow-y: auto;\n  display: flex;\n  grid-area: sidebar;\n  flex-direction: column;\n\n  height: calc(100vh - var(--header-height) - var(--player-height));\n  padding: var(--header-height) 30px 0;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Sidebar/Sidebar.tsx",
    "content": "import { MenuLinks } from './MenuLinks'\nimport s from './Sidebar.module.css'\n\nexport const Sidebar = () => {\n  return (\n    <div className={s.sidebar}>\n      <MenuLinks />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/layout/Sidebar/index.ts",
    "content": "export { Sidebar } from './Sidebar'\n"
  },
  {
    "path": "apps/rtk-query/src/layout/index.ts",
    "content": "export { Layout } from './Layout'\n"
  },
  {
    "path": "apps/rtk-query/src/main.tsx",
    "content": "import './styles/fonts.css'\nimport './styles/variables.css'\nimport './styles/reset.css'\nimport './styles/global.css'\n\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\n\nimport { App } from './app/App.tsx'\n\nconsole.log(import.meta.env.VITE_VERSION)\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/pages/MainPage/MainPage.module.css",
    "content": ".mainPage {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n}\n\n.artistsList {\n  --list-gap: 24px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/MainPage/MainPage.tsx",
    "content": "import { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\n\nimport { useMeQuery } from '@/features/auth'\nimport {\n  PlaylistActions,\n  PlaylistCard,\n  PlaylistCardSkeleton,\n  useFetchPlaylistsQuery,\n} from '@/features/playlists'\nimport { TagsList, useFindTagsQuery } from '@/features/tags'\nimport { type IncludedArtist, TrackCard, useFetchTracksQuery } from '@/features/tracks'\nimport { selectCurrentPlaylistId, usePlayerControls, useQueueControls } from '@/player'\nimport { convertApiTracksToPlayerTracks } from '@/player/utils/convert-api-track-to-player-track.ts'\nimport { useAppSelector } from '@/shared/hooks'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { ContentList, PageWithHeader } from '../common'\nimport s from './MainPage.module.css'\n\nconst NEW_TRACKS_PLAYLIST_ID = 'new-tracks'\n\nconst getArtistsByTrack = (\n  track: { relationships: { artists: { data: { id: string }[] } } },\n  included: IncludedArtist[]\n): string => {\n  const artistIds = track.relationships.artists.data.map((a) => a.id)\n  return included\n    .filter((artist) => artistIds.includes(artist.id))\n    .map((artist) => artist.attributes.name)\n    .join(', ')\n}\n\nexport const MainPage = () => {\n  const { t } = useTranslation()\n  const { data: me } = useMeQuery()\n  const isOwnPlaylist = (userId: string): boolean => me?.userId === userId\n  const { loadPlaylist } = useQueueControls()\n  const { play } = usePlayerControls()\n  const playerPlaylistId = useAppSelector(selectCurrentPlaylistId)\n\n  const { data: playlists, isLoading: isPlaylistsLoading } = useFetchPlaylistsQuery({\n    pageSize: 10,\n  })\n\n  const { data: tracks } = useFetchTracksQuery({\n    pageSize: 10,\n    pageNumber: 1,\n  })\n\n  const { data: tags } = useFindTagsQuery({ value: '' })\n\n  const playerTracks = useMemo(\n    () => tracks && convertApiTracksToPlayerTracks(tracks.data),\n    [tracks]\n  )\n\n  const handleTrackCardPlaybackClick = (trackId: string) => {\n    if (!playerTracks) {\n      return\n    }\n    if (playerPlaylistId !== NEW_TRACKS_PLAYLIST_ID) {\n      const playerTrackIndex = playerTracks.findIndex((track) => track.id === trackId)\n      loadPlaylist(NEW_TRACKS_PLAYLIST_ID, playerTracks, playerTrackIndex)\n    }\n    const playerTrack = playerTracks.find((track) => track.id === trackId)\n    if (playerTrack) {\n      play(playerTrack, NEW_TRACKS_PLAYLIST_ID)\n    }\n  }\n\n  return (\n    <PageWithHeader className={s.mainPage}>\n      <TagsList tags={tags || []} />\n      <ContentList\n        isLoading={isPlaylistsLoading}\n        skeleton={<PlaylistCardSkeleton showReactionButtons />}\n        title={t('playlists.title.new_playlists')}\n        data={playlists?.data}\n        renderItem={(playlist) => {\n          const image = getImageByType(playlist.attributes.images, ImageType.MEDIUM)\n          return (\n            <PlaylistCard\n              id={playlist.id}\n              title={playlist.attributes.title}\n              imageSrc={image?.url}\n              isShowReactionButtons={true}\n              reaction={playlist.attributes.currentUserReaction}\n              likesCount={playlist.attributes.likesCount}\n              userName={playlist.attributes.user.name}\n              userId={playlist.attributes.user.id}\n              addedAt={playlist.attributes.addedAt}\n              tracksCount={playlist.attributes.tracksCount}\n              shouldShowOwnerName\n              shouldShowCreatedDate\n              actions={\n                isOwnPlaylist(playlist.attributes.user.id) && (\n                  <PlaylistActions playlistId={playlist.id} />\n                )\n              }\n            />\n          )\n        }}\n      />\n\n      <ContentList\n        title={t('tracks.title.new_tracks')}\n        data={tracks?.data}\n        renderItem={(track) => (\n          <TrackCard\n            track={track}\n            artists={getArtistsByTrack(track, tracks?.included || [])}\n            handleTrackCardPlaybackClick={handleTrackCardPlaybackClick}\n          />\n        )}\n      />\n    </PageWithHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/MainPage/index.ts",
    "content": "export * from './MainPage'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/PlaylistPage.module.css",
    "content": ".playlistOverview {\n  margin-bottom: 46px;\n}\n\n.playlistToolbar {\n  display: flex;\n  gap: 36px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.errorMessage {\n  text-align: center;\n  font-size: var(--font-size-xxxl);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/PlaylistPage.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { useParams } from 'react-router'\n\nimport { useMeQuery } from '@/features/auth'\nimport { PlaylistOverview, useFetchPlaylistByIdQuery } from '@/features/playlists'\nimport { TrackRowContainer, TracksTable, useFetchTracksInPlaylistQuery } from '@/features/tracks'\nimport { usePageBackgroundColor, usePageSearchParams } from '@/pages/common/hooks'\nimport { usePlayerControls, useQueueControls } from '@/player'\nimport { convertApiTracksToPlayerTracks } from '@/player/utils/convert-api-track-to-player-track'\nimport { Typography } from '@/shared/components'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { PageWithoutHeader, SearchTextField } from '../common'\nimport s from './PlaylistPage.module.css'\nimport { PlaylistRow } from '@/features/playlists/ui/PlaylistRow/PlaylistRow.tsx'\nimport { ControlPanel } from './ui/ControlPanel'\nimport { PlaylistPageSkeleton } from './ui/PlaylistPageSkeleton'\n\nexport const PlaylistPage = () => {\n  const { t } = useTranslation()\n  const { debouncedSearch } = usePageSearchParams()\n\n  const { id } = useParams()\n  const { data: playlist, isLoading: isPlaylistLoading, isSuccess } = useFetchPlaylistByIdQuery(id!)\n  const { data: me } = useMeQuery()\n\n  const isOwnPlaylist = me?.userId === playlist?.data.attributes.user.id\n\n  const { data: tracks, isLoading: isTracksLoading } = useFetchTracksInPlaylistQuery({\n    playlistId: id!,\n  })\n\n  // TODO: Implement client-side track sorting after backend fix (issue #160)\n  //! FIXME: temporary implementation until backend issue #210 is fixed\n  const filteredTracks =\n    tracks?.data.filter((track) =>\n      track.attributes.title.toLowerCase().includes(debouncedSearch.toLowerCase())\n    ) ?? []\n\n  const { play } = usePlayerControls()\n  const { loadPlaylist } = useQueueControls()\n\n  const handlePlayAll = () => {\n    if (filteredTracks.length === 0) {\n      return\n    }\n    const playerTracks = convertApiTracksToPlayerTracks(filteredTracks)\n    loadPlaylist(id!, playerTracks, 0)\n    play(playerTracks[0], id!)\n  }\n\n  const playlistCover =\n    playlist?.data.attributes.images &&\n    getImageByType(playlist?.data.attributes.images, ImageType.ORIGINAL)\n\n  const { dominantColor, canvasRef } = usePageBackgroundColor(playlistCover?.url, isSuccess)\n\n  if (isPlaylistLoading || isTracksLoading) {\n    return <PlaylistPageSkeleton />\n  }\n\n  if (!playlist) {\n    return (\n      <PageWithoutHeader className={s.trackPage}>\n        <Typography variant=\"h1\" className={s.errorMessage}>\n          {t('playlists.label.load_error')}\n        </Typography>\n      </PageWithoutHeader>\n    )\n  }\n\n  return (\n    <PageWithoutHeader backgroundColor={dominantColor || 'var(--color-bg-primary)'}>\n      <canvas ref={canvasRef} style={{ display: 'none' }} />\n\n      <PlaylistOverview\n        className={s.playlistOverview}\n        title={playlist.data.attributes.title}\n        image={playlistCover?.url}\n        description={playlist.data.attributes.description || ''}\n        tags={playlist.data.attributes.tags}\n        userName={playlist.data.attributes.user.name}\n        tracksCount={playlist.data.attributes.tracksCount}\n      />\n      <div className={s.playlistToolbar}>\n        <SearchTextField placeholder={t('tracks.placeholder.search_tracks')} onChange={() => {}} />\n        <ControlPanel\n          className={s.playlistActions}\n          playlistId={playlist.data.id}\n          isOwnPlaylist={isOwnPlaylist}\n          reaction={playlist.data.attributes.currentUserReaction}\n          likesCount={playlist.data.attributes.likesCount}\n          onPlayAll={handlePlayAll}\n        />\n      </div>\n      {filteredTracks?.length > 0 ? (\n        <TracksTable\n          trackRows={filteredTracks.map((track, index) => ({\n            index,\n            id: track.id,\n            title: track.attributes.title,\n            imageSrc: getImageByType(track.attributes.images, ImageType.THUMBNAIL)?.url,\n            addedAt: track.attributes.addedAt,\n            artists: ['Artist 1', 'Artist 2'],\n            duration: 100,\n            likesCount: track.attributes.likesCount,\n            dislikesCount: track.attributes.dislikesCount,\n            currentUserReaction: track.attributes.currentUserReaction,\n            url: track.attributes.attachments[0].url,\n            isPublished: track.attributes.isPublished,\n          }))}\n          renderTrackRow={(trackRow) => (\n            <TrackRowContainer\n              key={trackRow.id}\n              trackRow={trackRow}\n              userId={me?.userId}\n              playlistId={playlist.data.id}\n            />\n          )}\n        />\n      ) : (\n        <div>{t('tracks.label.no_tracks')}</div>\n      )}\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/index.ts",
    "content": "export * from './PlaylistPage'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/ui/ControlPanel/ControlPanel.module.css",
    "content": ".box {\n  display: flex;\n  gap: 24px;\n  align-items: center;\n}\n\n.playButton {\n  width: 80px;\n  height: 80px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/ui/ControlPanel/ControlPanel.tsx",
    "content": "import clsx from 'clsx'\nimport { useTranslation } from 'react-i18next'\n\nimport {\n  useDislikePlaylistMutation,\n  useEditPlaylistModal,\n  useLikePlaylistMutation,\n  useUnReactionPlaylistMutation,\n} from '@/features/playlists'\nimport {\n  CurrentUserReaction,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  IconButton,\n  ReactionButtons,\n} from '@/shared/components'\nimport { EditIcon, MoreIcon, PlayIcon } from '@/shared/icons'\n\nimport s from './ControlPanel.module.css'\n\ntype ControlPanelProps = {\n  playlistId: string\n  isOwnPlaylist: boolean\n  reaction: CurrentUserReaction\n  likesCount: number\n  className?: string\n  onPlayAll?: () => void\n}\n\nexport const ControlPanel = ({\n  playlistId,\n  isOwnPlaylist,\n  reaction,\n  likesCount,\n  className,\n  onPlayAll,\n}: ControlPanelProps) => {\n  const { t } = useTranslation()\n\n  const [like] = useLikePlaylistMutation()\n  const [dislike] = useDislikePlaylistMutation()\n  const [unReaction] = useUnReactionPlaylistMutation()\n\n  const { handleOpenEditPlaylistModal } = useEditPlaylistModal()\n\n  return (\n    <div className={clsx(s.box, className)}>\n      <IconButton className={s.playButton} onClick={onPlayAll}>\n        <PlayIcon />\n      </IconButton>\n\n      <ReactionButtons\n        reaction={reaction}\n        onLike={() => like({ id: playlistId })}\n        onDislike={() => dislike({ id: playlistId })}\n        onUnReaction={() => unReaction({ id: playlistId })}\n        likesCount={likesCount}\n        size=\"large\"\n      />\n\n      {isOwnPlaylist && (\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem\n              onClick={() => {\n                handleOpenEditPlaylistModal(playlistId)\n              }}>\n              <EditIcon />\n              <span>{t('button.edit')}</span>\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/ui/ControlPanel/index.ts",
    "content": "export * from './ControlPanel'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/ui/PlaylistPageSkeleton/PlaylistPageSkeleton.module.css",
    "content": ".playlistPage {\n  --page-gradient-color: #adbf22;\n}\n.playlistOverview {\n  margin-bottom: 46px;\n  display: flex;\n  gap: 24px;\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 40px;\n  min-width: 0;\n}\n\n.playlistToolbar {\n  display: flex;\n  gap: 36px;\n  align-items: center;\n  margin-bottom: 25px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/ui/PlaylistPageSkeleton/PlaylistPageSkeleton.tsx",
    "content": "import { Skeleton } from '@/shared/components'\nimport { PageWithoutHeader, SearchTextField } from '@/pages/common'\nimport s from './PlaylistPageSkeleton.module.css'\nimport { TracksTableSkeleton } from '@/features/tracks'\nimport { useTranslation } from 'react-i18next'\nimport { PLAYLIST_SKELETON_INFO_LINES, PLAYLIST_SKELETON_TABLE_ROWS } from '@/shared/constants'\n\nexport const PlaylistPageSkeleton = () => {\n  const { t } = useTranslation()\n\n  return (\n    <PageWithoutHeader className={s.playlistPage}>\n      <div className={s.playlistOverview}>\n        <div className={s.imageContainer}>\n          <Skeleton height={'300px'} width={'300px'} />\n        </div>\n        <div className={s.content}>\n          <Skeleton height={'35px'} width={'400px'} />\n          <Skeleton width={'500px'} height={'55px'} />\n\n          <div className={s.info}>\n            {Array.from({ length: PLAYLIST_SKELETON_INFO_LINES }).map((_el, i) => (\n              <Skeleton height={'25px'} key={i} />\n            ))}\n          </div>\n        </div>\n      </div>\n      <div className={s.playlistToolbar}>\n        <SearchTextField placeholder={t('tracks.placeholder.search_tracks')} onChange={() => {}} />\n        <Skeleton width={'25%'} height={'60px'} />\n      </div>\n      <TracksTableSkeleton count={PLAYLIST_SKELETON_TABLE_ROWS} />\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistPage/ui/PlaylistPageSkeleton/index.ts",
    "content": "export * from './PlaylistPageSkeleton'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistsPage/PlaylistsPage.module.css",
    "content": ".title {\n  margin-bottom: 24px;\n}\n\n.pagination {\n  margin-top: 32px;\n}\n\n.controls {\n  margin-bottom: 32px;\n}\n\n.controlsRow {\n  display: flex;\n  gap: 32px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 32px;\n}\n\n.searchTags {\n  max-width: 513px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistsPage/PlaylistsPage.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { useMeQuery } from '@/features/auth'\nimport {\n  PlaylistActions,\n  PlaylistCard,\n  PlaylistCardSkeleton,\n  useFetchPlaylistsQuery,\n} from '@/features/playlists'\nimport { Pagination, Typography } from '@/shared/components'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { ContentList, PageWithHeader, SearchTags, SearchTextField, SortSelect } from '../common'\nimport { usePageSearchParams } from '../common/hooks'\nimport s from './PlaylistsPage.module.css'\n\nexport const PlaylistsPage = () => {\n  const { t } = useTranslation()\n  const { data: me } = useMeQuery()\n  const isOwnPlaylist = (userId: string): boolean => me?.userId === userId\n\n  const { pageNumber, handlePageChange, debouncedSearch, sortBy, sortDirection, tagsIds } =\n    usePageSearchParams()\n\n  const { data: playlists, isLoading: isPlaylistsLoading } = useFetchPlaylistsQuery({\n    pageNumber,\n    sortBy,\n    sortDirection,\n    search: debouncedSearch,\n    ...(tagsIds.length > 0 && { tagsIds }),\n  })\n  const pagesCount = playlists?.meta.pagesCount || 1\n\n  return (\n    <PageWithHeader>\n      <Typography variant=\"h2\" as=\"h1\" className={s.title}>\n        {t('playlists.title.all_playlists')}\n      </Typography>\n      <div className={s.controls}>\n        <div className={s.controlsRow}>\n          <SearchTextField placeholder={t('playlists.placeholder.search_playlist')} />\n          <SortSelect />\n        </div>\n        <SearchTags type=\"tags\" className={s.searchTags} />\n      </div>\n\n      <ContentList\n        data={playlists?.data}\n        isLoading={isPlaylistsLoading}\n        skeleton={<PlaylistCardSkeleton showReactionButtons />}\n        renderItem={(playlist) => {\n          const image = getImageByType(playlist.attributes.images, ImageType.MEDIUM)\n\n          return (\n            <PlaylistCard\n              id={playlist.id}\n              title={playlist.attributes.title}\n              imageSrc={image?.url}\n              isShowReactionButtons={true}\n              reaction={playlist.attributes.currentUserReaction}\n              likesCount={playlist.attributes.likesCount}\n              userName={playlist.attributes.user.name}\n              userId={playlist.attributes.user.id}\n              addedAt={playlist.attributes.addedAt}\n              shouldShowOwnerName\n              shouldShowCreatedDate\n              actions={\n                isOwnPlaylist(playlist.attributes.user.id) && (\n                  <PlaylistActions playlistId={playlist.id} />\n                )\n              }\n            />\n          )\n        }}\n      />\n\n      <Pagination\n        className={s.pagination}\n        page={pageNumber}\n        pagesCount={pagesCount}\n        onPageChange={handlePageChange}\n      />\n    </PageWithHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/PlaylistsPage/index.ts",
    "content": "export * from './PlaylistsPage'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackLyricsPage/TrackLyricsPage.module.css",
    "content": ".trackLyricsPage {\n  position: relative;\n}\n\n.header {\n  height: var(--header-height);\n}\n\n.trackTextWrapper {\n  padding-inline: 180px;\n  text-align: center;\n  white-space: wrap;\n}\n\n.button {\n  all: unset;\n\n  cursor: pointer;\n\n  position: absolute;\n  z-index: 2;\n  top: 20px;\n\n  display: flex;\n  gap: 12px;\n  align-items: center;\n\n  font-size: var(--font-size-m);\n  font-weight: 700;\n  color: var(--color-text-secondary);\n\n  transition: color 0.2s ease;\n}\n\n.button:hover {\n  color: var(--color-text-primary);\n}\n\n.trackText {\n  margin: 0;\n  font-size: var(--font-size-xxxxl);\n  font-weight: 900;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackLyricsPage/TrackLyricsPage.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { useNavigate, useParams } from 'react-router'\n\nimport { useFetchTrackByIdQuery } from '@/features/tracks'\nimport { PageWithoutHeader } from '@/pages/common'\nimport { usePageBackgroundColor } from '@/pages/common/hooks'\nimport { ArrowBackIcon } from '@/shared/icons/ArrowBackIcon.tsx'\nimport { ImageType } from '@/shared/types'\nimport { getImageByType } from '@/shared/utils'\n\nimport s from './TrackLyricsPage.module.css'\n\nexport const TrackLyricsPage = () => {\n  const { t } = useTranslation()\n  const { id } = useParams()\n  const navigate = useNavigate()\n\n  const { data: track, isLoading, isSuccess } = useFetchTrackByIdQuery({ trackId: id! })\n\n  const trackCover =\n    track?.data.attributes.images &&\n    getImageByType(track?.data.attributes.images, ImageType.ORIGINAL)\n\n  const { dominantColor, canvasRef } = usePageBackgroundColor(trackCover?.url, isSuccess)\n\n  const trackText = track?.data.attributes.lyrics || t('tracks.placeholder.no_lyrics')\n\n  return (\n    <PageWithoutHeader className={s.trackLyricsPage} backgroundColor={dominantColor}>\n      <canvas ref={canvasRef} style={{ display: 'none' }} />\n      {dominantColor && (\n        <>\n          <button\n            type=\"button\"\n            className={s.button}\n            onClick={() => {\n              navigate(-1)\n            }}>\n            <ArrowBackIcon />\n            {t('tracks.button.go_back')}\n          </button>\n\n          <div className={s.trackTextWrapper}>\n            {!isLoading && <p className={s.trackText}>{trackText}</p>}\n          </div>\n        </>\n      )}\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackLyricsPage/index.ts",
    "content": "export { TrackLyricsPage } from './TrackLyricsPage.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/TrackPage.module.css",
    "content": ".trackOverview {\n  margin-bottom: 46px;\n}\n\n.title {\n  margin-bottom: 18px;\n}\n\n.search {\n  margin-bottom: 24px;\n}\n\n.errorMessage {\n  text-align: center;\n  font-size: var(--font-size-xxxl);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/TrackPage.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { useParams } from 'react-router'\n\nimport { useMeQuery } from '@/features/auth'\nimport { useFetchPlaylistsQuery } from '@/features/playlists'\nimport { TrackOverview, useFetchTrackByIdQuery } from '@/features/tracks'\nimport { usePageBackgroundColor, usePageSearchParams } from '@/pages/common/hooks'\nimport type { Track } from '@/player'\nimport { Pagination, Typography } from '@/shared/components'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { ContentList, PageWithoutHeader, SearchTextField } from '../common'\nimport s from './TrackPage.module.css'\nimport { PlaylistRow } from '@/features/playlists/ui/PlaylistRow/PlaylistRow.tsx'\nimport { ControlPanel } from './ui/ControlPanel'\nimport { TrackPageSkeleton } from './ui/TrackPageSkeleton'\n\nexport const TrackPage = () => {\n  const { t } = useTranslation()\n\n  const { id } = useParams()\n  const {\n    data: track,\n    isLoading: isTrackLoading,\n    isSuccess,\n  } = useFetchTrackByIdQuery({ trackId: id! })\n  const { data: me } = useMeQuery()\n  const isTrackOwner = me?.userId === track?.data.attributes.user.id\n\n  // TODO: backend don't return user id for track\n\n  const { pageNumber, handlePageChange, debouncedSearch } = usePageSearchParams()\n\n  const { data: playlists, isLoading: isPlaylistsLoading } = useFetchPlaylistsQuery({\n    trackId: id!,\n    pageNumber,\n    pageSize: 4,\n    search: debouncedSearch,\n  })\n\n  const pagesCount = playlists?.meta.pagesCount || 1\n\n  const trackCover =\n    track?.data.attributes.images &&\n    getImageByType(track?.data.attributes.images, ImageType.ORIGINAL)\n\n  const { dominantColor, canvasRef } = usePageBackgroundColor(trackCover?.url, isSuccess)\n  if (isTrackLoading || isPlaylistsLoading) {\n    return <TrackPageSkeleton />\n  }\n\n  if (!track) {\n    return (\n      <PageWithoutHeader className={s.trackPage}>\n        <Typography variant=\"h1\" className={s.errorMessage}>\n          {t('tracks.label.load_error')}\n        </Typography>\n      </PageWithoutHeader>\n    )\n  }\n\n  // Transform TrackDetails to Track type expected by player\n  const playerTrack: Track = {\n    id: track.data.id,\n    title: track.data.attributes.title,\n    artist: track.data.attributes.artists.map((artist) => artist.name).join(', '),\n    duration: track.data.attributes.duration,\n    url: track.data.attributes.attachments[0]?.url || '',\n    albumArt: trackCover?.url,\n  }\n\n  return (\n    <PageWithoutHeader backgroundColor={dominantColor || 'var(--color-bg-primary)'}>\n      <canvas ref={canvasRef} style={{ display: 'none' }} />\n\n      <TrackOverview\n        className={s.trackOverview}\n        title={track.data.attributes.title}\n        image={trackCover?.url}\n        addedAt={track.data.attributes.addedAt}\n        artists={track.data.attributes.artists.map((artist) => artist.name)}\n        tags={track.data.attributes.tags}\n      />\n\n      <ControlPanel\n        track={playerTrack}\n        trackId={track.data.id}\n        isOwnTrack={isTrackOwner}\n        isPublished={track.data.attributes.isPublished}\n        reaction={track.data.attributes.currentUserReaction}\n        likesCount={track.data.attributes.likesCount}\n      />\n\n      <Typography variant=\"h2\" className={s.title}>\n        {t('placeholder.which_playlist')}\n      </Typography>\n      <SearchTextField placeholder={t('playlists.placeholder.search_playlist')} />\n      {playlists?.data && (\n        <ContentList\n          layout={'row'}\n          data={playlists.data}\n          emptyMessage={t('playlists.title.playlists_not_found')}\n          renderItem={(playlist) => (\n            <PlaylistRow\n              key={playlist.id}\n              id={playlist.id}\n              title={playlist.attributes.title}\n              imageSrc={getImageByType(playlist.attributes.images, ImageType.ORIGINAL)?.url}\n            />\n          )}\n        />\n      )}\n      <Pagination\n        className={s.pagination}\n        page={pageNumber}\n        pagesCount={pagesCount}\n        onPageChange={handlePageChange}\n      />\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/index.ts",
    "content": "export * from './TrackPage'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/ui/ControlPanel/ControlPanel.module.css",
    "content": ".box {\n  display: flex;\n  gap: 24px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.playButton {\n  width: 80px;\n  height: 80px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/ui/ControlPanel/ControlPanel.tsx",
    "content": "import { TrackActions, useLazyFetchTrackByIdQuery } from '@/features/tracks'\nimport { type Track, useCurrentTrack, usePlaybackState, usePlayerControls } from '@/player'\nimport { CurrentUserReaction, IconButton } from '@/shared/components'\nimport { PauseIcon, PlayIcon } from '@/shared/icons'\n\nimport s from './ControlPanel.module.css'\n\nexport const ControlPanel = ({\n  trackId,\n  isOwnTrack,\n  isPublished,\n  reaction,\n  likesCount,\n  track,\n}: {\n  track: Track\n  trackId: string\n  isOwnTrack: boolean\n  isPublished: boolean\n  reaction: CurrentUserReaction\n  likesCount: number\n}) => {\n  const { play, pause, resume } = usePlayerControls()\n  const { isPlaying } = usePlaybackState()\n  const { track: currentTrack } = useCurrentTrack()\n  const [fetchTrack] = useLazyFetchTrackByIdQuery()\n\n  const onClickHandler = async () => {\n    if (currentTrack && currentTrack.id === track.id) {\n      if (isPlaying) {\n        pause()\n      } else {\n        resume()\n      }\n    } else {\n      try {\n        const result = await fetchTrack({ trackId: trackId })\n        if (result.data?.data) {\n          const fullTrack: Track = {\n            id: result.data.data.id,\n            title: result.data.data.attributes.title,\n            artist: result.data.data.attributes.artists[0]?.name || 'Unknown Artist',\n            duration: result.data.data.attributes.duration,\n            url: result.data.data.attributes.attachments[0]?.url || '',\n            albumArt: result.data.data.attributes.images?.main?.[0]?.url,\n          }\n          play(fullTrack)\n        }\n      } catch (error) {\n        console.error('Failed to fetch track:', error)\n        // Fallback to the track we already have\n        play(track)\n      }\n    }\n  }\n\n  const isCurrentTrack = currentTrack && currentTrack.id === track.id\n\n  return (\n    <div className={s.box}>\n      <IconButton onClick={onClickHandler} className={s.playButton}>\n        {isCurrentTrack && isPlaying ? <PauseIcon /> : <PlayIcon />}\n      </IconButton>\n\n      <TrackActions\n        trackId={trackId}\n        reaction={reaction}\n        likesCount={likesCount}\n        sizeReactionButtons=\"large\"\n        isOwner={isOwnTrack}\n        isPublished={isPublished}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/ui/ControlPanel/index.ts",
    "content": "export * from './ControlPanel'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/ui/TrackPageSkeleton/TrackPageSkeleton.module.css",
    "content": ".trackPage {\n  --page-gradient-color: #9a3426;\n}\n\n.trackOverview {\n  margin-bottom: 46px;\n  display: flex;\n  gap: 24px;\n  height: 300px;\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 30px;\n  min-width: 0;\n}\n\n.title {\n  padding-top: 16px;\n  margin-bottom: 18px;\n}\n\n.playlists {\n  padding-top: 40px;\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/ui/TrackPageSkeleton/TrackPageSkeleton.tsx",
    "content": "import { Skeleton, Typography } from '@/shared/components'\nimport s from './TrackPageSkeleton.module.css'\nimport { PageWithoutHeader, SearchTextField } from '@/pages/common'\nimport { useTranslation } from 'react-i18next'\nimport { TRACK_SKELETON_INFO_LINES, TRACK_SKELETON_PLAYLISTS } from '@/shared/constants'\n\nexport const TrackPageSkeleton = () => {\n  const { t } = useTranslation()\n\n  return (\n    <PageWithoutHeader className={s.trackPage}>\n      <div className={s.trackOverview}>\n        <div className={s.imageContainer}>\n          <Skeleton height={'300px'} width={'300px'} />\n        </div>\n        <div className={s.content}>\n          <Skeleton height={'35px'} width={'400px'} />\n          <Skeleton width={'500px'} height={'55px'} />\n\n          <div className={s.info}>\n            {Array.from({ length: TRACK_SKELETON_INFO_LINES }).map((_el, i) => (\n              <Skeleton height={'25px'} key={i} />\n            ))}\n          </div>\n          <Skeleton width={'150px'} height={'30px'} />\n        </div>\n      </div>\n\n      <Skeleton width={'300px'} height={'70px'} />\n\n      <Typography variant=\"h2\" className={s.title}>\n        {t('placeholder.which_playlist')}\n      </Typography>\n\n      <SearchTextField placeholder={t('playlists.placeholder.search_playlist')} />\n\n      <div className={s.playlists}>\n        {Array.from({ length: TRACK_SKELETON_PLAYLISTS }).map((_el, i) => (\n          <Skeleton height={'70px'} width={'100%'} key={i} />\n        ))}\n      </div>\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TrackPage/ui/TrackPageSkeleton/index.ts",
    "content": "export * from './TrackPageSkeleton'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TracksPage/TracksPage.module.css",
    "content": ".title {\n  margin-bottom: 24px;\n}\n\n.controls {\n  margin-bottom: 32px;\n}\n\n.controlsRow {\n  display: flex;\n  gap: 32px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 32px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TracksPage/TracksPage.tsx",
    "content": "import { useEffect } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { useInView } from 'react-intersection-observer'\n\nimport { useMeQuery } from '@/features/auth'\nimport {\n  TracksTable,\n  TracksTableSkeleton,\n  useFetchTracksByScrollInfiniteQuery,\n} from '@/features/tracks'\nimport { TrackActions } from '@/features/tracks/ui/TrackActions/TrackActions'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { usePlaybackState, usePlayerControls } from '@/player'\nimport { useCurrentTrack, useQueueControls } from '@/player/playerHooks.ts'\nimport {\n  convertApiTracksToPlayerTracks,\n  convertApiTrackToPlayerTrack,\n} from '@/player/utils/convert-api-track-to-player-track.ts'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { Typography } from '@/shared/components'\nimport { Spinner } from '@/shared/components/Spinner/Spinner.tsx'\nimport { useAppSelector } from '@/shared/hooks/useAppSelector.ts'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { PageWithHeader, SearchTags, SearchTextField, SortSelect } from '../common'\nimport { usePageSearchParams } from '../common/hooks'\nimport s from './TracksPage.module.css'\n\nexport const TracksPage = () => {\n  const { t } = useTranslation()\n\n  const { debouncedSearch, sortBy, sortDirection, tagsIds, artistsIds } = usePageSearchParams()\n\n  const {\n    data: tracksData,\n    hasNextPage,\n    isFetchingNextPage,\n    fetchNextPage,\n    isLoading,\n  } = useFetchTracksByScrollInfiniteQuery({\n    search: debouncedSearch,\n    sortBy,\n    sortDirection,\n    tagsIds,\n    artistsIds,\n  })\n  const pages = tracksData?.pages.flatMap((p) => p.data) || []\n\n  const { data: me } = useMeQuery()\n\n  const { play, resume, pause } = usePlayerControls()\n  const { loadPlaylist, addToQueue } = useQueueControls()\n  const { track: currentTrack } = useCurrentTrack()\n  const { isPlaying } = usePlaybackState()\n\n  const currentPlaylistId = useAppSelector((state) => state.player.currentPlaylistId)\n\n  const handleTrackPlayClick = (trackId: string) => {\n    const clickedTrack = pages.find((track) => track.id === trackId)\n\n    if (!clickedTrack) {\n      return\n    }\n\n    if (currentTrack?.id === trackId) {\n      if (isPlaying) {\n        pause()\n      } else {\n        resume()\n      }\n\n      return\n    }\n\n    const playerTrack = convertApiTrackToPlayerTrack(clickedTrack)\n    const tracksForPlayer = convertApiTracksToPlayerTracks(pages)\n\n    play(playerTrack, 'all-tracks', tracksForPlayer)\n  }\n\n  const { ref, inView } = useInView({\n    threshold: 0.1,\n  })\n\n  useEffect(() => {\n    // Handle infinite scroll loading\n    if (inView && hasNextPage && !isFetchingNextPage) {\n      fetchNextPage()\n    }\n\n    // Update player queue when new tracks are loaded\n    if (tracksData?.pages) {\n      const allTracks = tracksData.pages.flatMap((page) => page.data)\n      const playerTracks = convertApiTracksToPlayerTracks(allTracks)\n\n      if (playerTracks.length > 0) {\n        if (currentPlaylistId === 'all-tracks') {\n          // Playlist already exists, add new tracks\n          // We need to get only newly added tracks\n          if (tracksData.pages.length > 1) {\n            const currentPageIndex = tracksData.pages.length - 1\n            const newTracks = tracksData.pages[currentPageIndex].data\n            const newPlayerTracks = convertApiTracksToPlayerTracks(newTracks)\n            addToQueue(newPlayerTracks)\n          }\n        } else {\n          // First load - initialize playlist\n          loadPlaylist('all-tracks', playerTracks)\n        }\n      }\n    }\n  }, [\n    inView,\n    hasNextPage,\n    isFetchingNextPage,\n    fetchNextPage,\n    tracksData?.pages,\n    addToQueue,\n    loadPlaylist,\n    currentPlaylistId,\n  ])\n\n  return (\n    <PageWithHeader>\n      <Typography variant=\"h2\" as=\"h1\" className={s.title}>\n        {t('tracks.title.all_tracks')}\n      </Typography>\n      <div className={s.controls}>\n        <div className={s.controlsRow}>\n          <SearchTextField placeholder={t('tracks.placeholder.search_tracks')} />\n          <SortSelect />\n        </div>\n        <div className={s.controlsRow}>\n          <SearchTags type=\"tags\" />\n          <SearchTags type=\"artists\" />\n        </div>\n      </div>\n\n      {isLoading ? (\n        <TracksTableSkeleton />\n      ) : (\n        <TracksTable\n          trackRows={pages.map((track, index) => {\n            const image = getImageByType(track.attributes.images, ImageType.MEDIUM)\n            const userId = track.attributes.user.id\n            const isOwner = userId === me?.userId\n\n            return {\n              index,\n              id: track.id,\n              title: track.attributes.title,\n              imageSrc: image?.url || noCoverPlaceholder,\n              addedAt: track.attributes.addedAt,\n              artists: ['Artist 1', 'Artist 2'],\n              duration: 100,\n              likesCount: track.attributes.likesCount,\n              dislikesCount: track.attributes.dislikesCount,\n              currentUserReaction: track.attributes.currentUserReaction,\n              url: track.attributes.attachments[0].url,\n              isOwner,\n              isPublished: track.attributes.isPublished,\n            }\n          })}\n          renderTrackRow={(trackRow) => (\n            <TrackRow\n              key={trackRow.id}\n              trackRow={trackRow}\n              onTrackPlayClick={handleTrackPlayClick}\n              renderActionsCell={() => (\n                <TrackActions\n                  reaction={trackRow.currentUserReaction}\n                  likesCount={trackRow.likesCount}\n                  trackId={trackRow.id}\n                  isOwner={trackRow.isOwner}\n                  isPublished={trackRow.isPublished}\n                />\n              )}\n            />\n          )}\n        />\n      )}\n\n      {hasNextPage && (\n        <div ref={ref}>\n          {isFetchingNextPage ? <Spinner size={50} /> : <div style={{ height: '10px' }} />}\n        </div>\n      )}\n      {!hasNextPage && pages.length > 0 && <p>Nothing more to load</p>}\n    </PageWithHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/TracksPage/index.ts",
    "content": "export * from './TracksPage'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/UserPage.module.css",
    "content": ".userPage {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/UserPage.tsx",
    "content": "import { PageWithoutHeader } from '@/pages/common'\nimport { useUserPageBackgroundColor } from '@/pages/UserPage/hooks'\n\nimport { UserInfo, UserTabs } from './ui'\nimport s from './UserPage.module.css'\n\nexport const UserPage = () => {\n  const { dominantColor, canvasRef } = useUserPageBackgroundColor()\n\n  return (\n    <PageWithoutHeader className={s.userPage} backgroundColor={dominantColor}>\n      <canvas ref={canvasRef} style={{ display: 'none' }} />\n      {dominantColor && (\n        <>\n          <UserInfo />\n          <UserTabs />\n        </>\n      )}\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/hooks/index.ts",
    "content": "export * from './useOwnerData.ts'\nexport * from './useUserPageBackgroundColor.ts'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/hooks/useOwnerData.ts",
    "content": "import { useParams } from 'react-router'\n\nimport { useMeQuery } from '@/features/auth'\nimport { useFetchPlaylistsQuery } from '@/features/playlists'\nimport { useFetchTracksQuery } from '@/features/tracks'\n\nexport const useOwnerData = () => {\n  const { data: user, isLoading: isMeLoading } = useMeQuery()\n  const { userId: pageOwnerId } = useParams()\n  const isProfileOwner = user?.userId === pageOwnerId\n\n  const { data: tracksResponse, isLoading: isTracksLoading } = useFetchTracksQuery(\n    {\n      pageSize: 1,\n      pageNumber: 1,\n      userId: pageOwnerId,\n    },\n    { skip: isMeLoading || !pageOwnerId }\n  )\n\n  const { data: playlistsResponse, isLoading: isPlaylistsLoading } = useFetchPlaylistsQuery(\n    { userId: pageOwnerId, pageSize: 1 },\n    { skip: isMeLoading || !pageOwnerId }\n  )\n\n  let userLogin = isProfileOwner ? user?.login : ''\n\n  if (!isProfileOwner && playlistsResponse?.data?.[0]) {\n    userLogin = playlistsResponse.data[0].attributes.user.name\n  }\n\n  if (!isProfileOwner && !userLogin && tracksResponse?.data?.[0]) {\n    userLogin = tracksResponse.data[0].attributes.user.name\n  }\n\n  return {\n    isProfileOwner,\n    userLogin,\n    playlistsCount: playlistsResponse?.meta.totalCount || 0,\n    tracksCount: tracksResponse?.meta.totalCount || 0,\n    isInitialLoading: isMeLoading || isPlaylistsLoading || isTracksLoading,\n    isContentLoading: isPlaylistsLoading || isTracksLoading || isMeLoading,\n    isMeQuerySuccess: !isMeLoading,\n    pageOwnerId,\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/hooks/useUserPageBackgroundColor.ts",
    "content": "import { useMemo } from 'react'\n\nimport { selectProfileAvatar } from '@/features/profile'\nimport { usePageBackgroundColor } from '@/pages/common/hooks'\nimport { useOwnerData } from '@/pages/UserPage/hooks/useOwnerData.ts'\nimport { useAppSelector } from '@/shared/hooks'\nimport { decodeFileFromBase64 } from '@/shared/utils'\n\nexport const useUserPageBackgroundColor = () => {\n  const { isProfileOwner, isMeQuerySuccess } = useOwnerData()\n  const profileAvatarUrl = useAppSelector(selectProfileAvatar)\n\n  const decodedProfileAvatarUrl = useMemo(\n    () => decodeFileFromBase64(profileAvatarUrl),\n    [profileAvatarUrl]\n  )\n  const imageUrlForBackgroundColor = isProfileOwner ? decodedProfileAvatarUrl : null\n  const isLocalUrlData = !!decodedProfileAvatarUrl\n  return usePageBackgroundColor(imageUrlForBackgroundColor, isMeQuerySuccess, isLocalUrlData)\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/index.ts",
    "content": "export * from './UserPage'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/UserInfo.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  padding-top: 16px;\n}\n\n.avatar {\n  overflow: hidden;\n  width: 96px;\n  height: 96px;\n  border-radius: 50%;\n}\n\n.userName {\n  margin-top: 8px;\n}\n\n.editButton {\n  margin-top: 6px;\n  border-radius: 4px;\n  font-size: 14px;\n}\n\n.stats {\n  margin-top: 10px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/UserInfo.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { selectProfileAvatar, selectProfileFullName, useEditProfileModal } from '@/features/profile'\nimport { useOwnerData } from '@/pages/UserPage/hooks'\nimport { UserStats } from './UserStats'\nimport { UserInfoSkeleton } from './UserInfoSkeleton'\nimport { Avatar, Button, Typography } from '@/shared/components'\nimport { useAppSelector } from '@/shared/hooks'\nimport { EditIcon } from '@/shared/icons'\n\nimport s from './UserInfo.module.css'\n\nexport const UserInfo = () => {\n  const { t } = useTranslation()\n\n  const { isProfileOwner, userLogin, playlistsCount, tracksCount, isInitialLoading } =\n    useOwnerData()\n\n  const { handleOpenEditProfileModal } = useEditProfileModal()\n  const profileAvatarUrl = useAppSelector(selectProfileAvatar)\n  const profileFullName = useAppSelector(selectProfileFullName)\n\n  const userFullName =\n    isProfileOwner && profileFullName.name\n      ? `${profileFullName.name} ${profileFullName.surname}`\n      : userLogin\n\n  if (isInitialLoading) {\n    return <UserInfoSkeleton />\n  }\n\n  return (\n    <div className={s.box}>\n      <Avatar\n        className={s.avatar}\n        src={isProfileOwner ? profileAvatarUrl : undefined}\n        fullName={isProfileOwner ? profileFullName : undefined}\n        userLogin={userLogin}\n      />\n      <Typography variant=\"h2\" className={s.userName}>\n        {userFullName}\n      </Typography>\n      {isProfileOwner && (\n        <Button className={s.editButton} variant=\"secondary\" onClick={handleOpenEditProfileModal}>\n          <EditIcon />\n          {t('button.edit_profile')}\n        </Button>\n      )}\n      <div className={s.stats}>\n        <UserStats playlistsCount={playlistsCount} tracksCount={tracksCount} />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/UserInfoSkeleton/UserInfoSkeleton.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 10px;\n  padding-top: 16px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/UserInfoSkeleton/UserInfoSkeleton.tsx",
    "content": "import { Skeleton } from '@/shared/components'\nimport s from './UserInfoSkeleton.module.css'\n\nexport const UserInfoSkeleton = () => {\n  return (\n    <div className={s.box}>\n      <Skeleton circle={true} width={'96px'} height={'96px'} />\n      <Skeleton height={'30px'} width={'180px'} />\n      <Skeleton height={'34px'} width={'140px'} />\n      <Skeleton height={'40px'} width={'190px'} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/UserInfoSkeleton/index.ts",
    "content": "export * from './UserInfoSkeleton.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/UserStats/UserStats.module.css",
    "content": ".descriptionList {\n  display: flex;\n  gap: 28px;\n  margin: 0;\n}\n\n.descriptionItem {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.descriptionItem dd {\n  margin: 0;\n}\n\n.descriptionItem dt {\n  font-size: var(--font-size-s);\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/UserStats/UserStats.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { Typography } from '@/shared/components'\n\nimport s from './UserStats.module.css'\n\ntype UserStatsProps = {\n  playlistsCount?: number\n  tracksCount?: number\n}\n\nexport const UserStats = ({ playlistsCount = 0, tracksCount = 0 }: UserStatsProps) => {\n  const { t } = useTranslation()\n\n  return (\n    <dl className={s.descriptionList}>\n      <div className={s.descriptionItem}>\n        <Typography as=\"dd\" variant=\"body1\">\n          {playlistsCount}\n        </Typography>\n        <Typography as=\"dt\" variant=\"body2\">\n          {t('profile.stats.playlists', { count: playlistsCount })}\n        </Typography>\n      </div>\n      <div className={s.descriptionItem}>\n        <Typography as=\"dd\" variant=\"body1\">\n          {tracksCount}\n        </Typography>\n        <Typography as=\"dt\" variant=\"body2\">\n          {t('profile.stats.tracks', { count: tracksCount })}\n        </Typography>\n      </div>\n    </dl>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/UserStats/index.ts",
    "content": "export * from './UserStats'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserInfo/index.ts",
    "content": "export * from './UserInfo'\nexport * from './UserStats'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/LikedTracksTab/LikedTracksTab.module.css",
    "content": ""
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/LikedTracksTab/LikedTracksTab.tsx",
    "content": "import { t } from 'i18next'\nimport { useMemo } from 'react'\n\nimport { TracksTable, useFetchTracksQuery } from '@/features/tracks'\nimport { TrackActions } from '@/features/tracks/ui/TrackActions/TrackActions'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { useOwnerData } from '@/pages/UserPage/hooks'\nimport { selectCurrentPlaylistId, usePlayerControls, useQueueControls } from '@/player'\nimport { convertApiTracksToPlayerTracks } from '@/player/utils/convert-api-track-to-player-track.ts'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { Pagination } from '@/shared/components'\nimport { useAppSelector } from '@/shared/hooks'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { usePageSearchParams } from '@/pages/common/hooks'\n\nexport const LikedTracksTab = () => {\n  const { pageOwnerId, isProfileOwner } = useOwnerData()\n  const { pageNumber, handlePageChange } = usePageSearchParams()\n\n  const { data: tracksResponse, isLoading } = useFetchTracksQuery({\n    userId: pageOwnerId,\n    pageNumber,\n    pageSize: 10,\n  })\n\n  const { play } = usePlayerControls()\n  const { loadPlaylist } = useQueueControls()\n  const playerPlaylistId = useAppSelector(selectCurrentPlaylistId)\n\n  const currentPlaylistId = `${pageOwnerId}-liked-tracks`\n  const playerTracks = useMemo(\n    () => tracksResponse && convertApiTracksToPlayerTracks(tracksResponse.data),\n    [tracksResponse]\n  )\n\n  const handleTrackPlayClick = (trackId: string) => {\n    if (!playerTracks) return\n    const playerTrackIndex = playerTracks.findIndex((track) => track.id === trackId)\n    if (playerPlaylistId !== currentPlaylistId) {\n      loadPlaylist(currentPlaylistId, playerTracks, playerTrackIndex)\n    }\n    const playerTrack = playerTracks.find((track) => track.id === trackId)\n    if (playerTrack) {\n      play(playerTrack, currentPlaylistId)\n    }\n  }\n\n  if (isLoading) return null\n\n  return (\n    <>\n      <TracksTable\n        trackRows={\n          tracksResponse?.data?.map((track, index) => {\n            const image = getImageByType(track.attributes.images, ImageType.MEDIUM)\n            return {\n              index,\n              id: track.id,\n              title: track.attributes.title,\n              imageSrc: image?.url || noCoverPlaceholder,\n              addedAt: track.attributes.addedAt,\n              artists: ['Artist 1', 'Artist 2'],\n              duration: 100,\n              likesCount: track.attributes.likesCount,\n              dislikesCount: track.attributes.dislikesCount,\n              currentUserReaction: track.attributes.currentUserReaction,\n              url: track.attributes.attachments[0]?.url || '',\n              isPublished: track.attributes.isPublished,\n            }\n          }) ?? []\n        }\n        renderTrackRow={(trackRow) => (\n          <TrackRow\n            key={trackRow.id}\n            trackRow={trackRow}\n            onTrackPlayClick={handleTrackPlayClick}\n            renderActionsCell={() => (\n              <TrackActions\n                trackId={trackRow.id}\n                isOwner={isProfileOwner}\n                isPublished={trackRow.isPublished}\n                reaction={undefined}\n                likesCount={undefined}\n              />\n            )}\n          />\n        )}\n      />\n      {tracksResponse && (\n        <Pagination\n          page={pageNumber}\n          pagesCount={tracksResponse.meta.pagesCount || 1}\n          onPageChange={handlePageChange}\n          alwaysVisible\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/LikedTracksTab/index.ts",
    "content": "export * from './LikedTracksTab'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/MyLikedPlaylistsTab.module.css",
    "content": ".playlistsList {\n  display: grid;\n  grid-template-columns: repeat(5, minmax(0, 1fr));\n  gap: 8px;\n}\n\n.playlistsList > li {\n  min-width: 0;\n}\n\n.playlistsList > li > * {\n  width: 100% !important;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/MyLikedPlaylistsTab.tsx",
    "content": "import { t } from 'i18next'\nimport { useParams, useSearchParams } from 'react-router'\n\nimport {\n  PlaylistCard,\n  useCreatePlaylistModal,\n  useEditPlaylistModal,\n  useFetchPlaylistsQuery,\n  useRemovePlaylistMutation,\n} from '@/features/playlists'\nimport { ContentList } from '@/pages/common'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Pagination,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\nimport s from './MyLikedPlaylistsTab.module.css'\n\nexport const MyLikedPlaylistsTab = () => {\n  const { userId } = useParams()\n\n  const { handleOpenEditPlaylistModal } = useEditPlaylistModal()\n  const [removePlaylist] = useRemovePlaylistMutation()\n\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const pageNumber = Number(searchParams.get('page')) || 1\n  const { data: playlists, isLoading } = useFetchPlaylistsQuery({ pageNumber, userId: userId! })\n  const pagesCount = playlists?.meta.pagesCount || 1\n\n  const handlePageChange = (page: number) => {\n    setSearchParams((prev) => {\n      if (page === 1) {\n        prev.delete('page')\n      } else {\n        prev.set('page', page.toString())\n      }\n      return prev\n    })\n  }\n\n  return (\n    <>\n      {playlists?.data && (\n        <ContentList\n          data={playlists?.data}\n          listClassName={s.playlistsList}\n          renderItem={(playlist) => {\n            const image = getImageByType(playlist.attributes.images, ImageType.MEDIUM)\n            return (\n              <PlaylistCard\n                id={playlist.id}\n                title={playlist.attributes.title}\n                imageSrc={image?.url}\n                userName={playlist.attributes.user.name}\n                isShowReactionButtons={true}\n                reaction={playlist.attributes.currentUserReaction}\n                likesCount={playlist.attributes.likesCount}\n                userId={playlist.attributes.user.id}\n                addedAt={playlist.attributes.addedAt}\n                tracksCount={playlist.attributes.tracksCount}\n                shouldShowOwnerName\n                shouldShowCreatedDate\n                actions={\n                  <DropdownMenu>\n                    <DropdownMenuTrigger>\n                      <MoreIcon />\n                    </DropdownMenuTrigger>\n                    <DropdownMenuContent>\n                      <DropdownMenuItem\n                        onClick={() => {\n                          handleOpenEditPlaylistModal(playlist.id)\n                        }}>\n                        {t('button.edit')}\n                      </DropdownMenuItem>\n                      <DropdownMenuItem\n                        onClick={() => {\n                          removePlaylist(playlist.id)\n                        }}>\n                        {t('button.delete')}\n                      </DropdownMenuItem>\n                    </DropdownMenuContent>\n                  </DropdownMenu>\n                }\n              />\n            )\n          }}\n        />\n      )}\n      {!isLoading && (\n        <Pagination\n          page={pageNumber}\n          pagesCount={pagesCount}\n          onPageChange={handlePageChange}\n          alwaysVisible\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/index.ts",
    "content": "export * from './MyLikedPlaylistsTab'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/PlaylistsTab/PlaylistsTab.module.css",
    "content": ".createPlaylistButton {\n  display: block;\n\n  width: 328px;\n  height: 54px;\n  margin: 0 auto;\n  margin-bottom: 24px;\n}\n\n.emptyState {\n  text-align: center;\n  padding: 60px 0;\n  color: #888;\n  font-size: 16px;\n}\n\n.playlistsList {\n  display: grid;\n  grid-template-columns: repeat(5, minmax(0, 1fr));\n  gap: 8px;\n}\n\n.playlistsList > li {\n  min-width: 0;\n}\n\n.playlistsList > li > * {\n  width: 100% !important;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/PlaylistsTab/PlaylistsTab.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport {\n  PlaylistActions,\n  PlaylistCard,\n  useCreatePlaylistModal,\n  useFetchPlaylistsQuery,\n} from '@/features/playlists'\nimport { ContentList } from '@/pages/common'\nimport { useOwnerData } from '@/pages/UserPage/hooks'\nimport { Button, Pagination } from '@/shared/components'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { usePageSearchParams } from '@/pages/common/hooks'\nimport s from './PlaylistsTab.module.css'\n\nexport const PlaylistsTab = () => {\n  const { t } = useTranslation()\n  const { isProfileOwner, pageOwnerId } = useOwnerData()\n  const { pageNumber, handlePageChange } = usePageSearchParams()\n\n  const { data: playlistsResponse, isLoading } = useFetchPlaylistsQuery({\n    userId: pageOwnerId,\n    pageNumber,\n    pageSize: 8,\n  })\n\n  const { handleOpenCreatePlaylistModal } = useCreatePlaylistModal()\n\n  if (isLoading) return null\n\n  return (\n    <>\n      {isProfileOwner && (\n        <Button className={s.createPlaylistButton} onClick={handleOpenCreatePlaylistModal}>\n          {t('playlists.button.create_playlist')}\n        </Button>\n      )}\n\n      {playlistsResponse?.data && playlistsResponse.data.length > 0 && (\n        <ContentList\n          data={playlistsResponse.data}\n          listClassName={s.playlistsList}\n          renderItem={(playlist) => {\n            const image = getImageByType(playlist.attributes.images, ImageType.MEDIUM)\n            return (\n              <PlaylistCard\n                id={playlist.id}\n                title={playlist.attributes.title}\n                imageSrc={image?.url}\n                tracksCount={playlist.attributes.tracksCount}\n                actions={isProfileOwner && <PlaylistActions playlistId={playlist.id} />}\n              />\n            )\n          }}\n        />\n      )}\n\n      {playlistsResponse && (\n        <Pagination\n          page={pageNumber}\n          pagesCount={playlistsResponse.meta.pagesCount || 1}\n          onPageChange={handlePageChange}\n          alwaysVisible\n        />\n      )}\n      {playlistsResponse?.data && playlistsResponse.data.length === 0 && (\n        <div className={s.emptyState}>{t('playlists.title.no_playlists')}</div>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/PlaylistsTab/index.ts",
    "content": "export * from './PlaylistsTab'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/TracksTab/TracksTab.module.css",
    "content": ".uploadTrackButton {\n  display: block;\n\n  width: 328px;\n  height: 54px;\n  margin: 0 auto;\n  margin-bottom: 24px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/TracksTab/TracksTab.tsx",
    "content": "import { t } from 'i18next'\nimport { useMemo } from 'react'\n\nimport { TracksTable, useCreateTrackModal, useFetchTracksQuery } from '@/features/tracks'\nimport { TrackActions } from '@/features/tracks/ui/TrackActions/TrackActions'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { useOwnerData } from '@/pages/UserPage/hooks'\nimport { selectCurrentPlaylistId, usePlayerControls, useQueueControls } from '@/player'\nimport { convertApiTracksToPlayerTracks } from '@/player/utils/convert-api-track-to-player-track.ts'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { Button, Pagination } from '@/shared/components'\nimport { useAppSelector } from '@/shared/hooks'\nimport { ImageType } from '@/shared/types/commonApi.types'\nimport { getImageByType } from '@/shared/utils'\n\nimport { usePageSearchParams } from '@/pages/common/hooks'\nimport s from './TracksTab.module.css'\n\nexport const TracksTab = () => {\n  const { isProfileOwner, pageOwnerId } = useOwnerData()\n  const { pageNumber, handlePageChange } = usePageSearchParams()\n\n  const { data: tracksResponse, isLoading } = useFetchTracksQuery({\n    userId: pageOwnerId,\n    pageNumber,\n    pageSize: 10,\n    includeDrafts: isProfileOwner ? true : undefined,\n  })\n\n  const { handleOpenCreateTrackModal } = useCreateTrackModal()\n  const { play } = usePlayerControls()\n  const { loadPlaylist } = useQueueControls()\n  const playerPlaylistId = useAppSelector(selectCurrentPlaylistId)\n\n  const currentPlaylistId = `${pageOwnerId}-user-tracks`\n  const playerTracks = useMemo(\n    () => tracksResponse && convertApiTracksToPlayerTracks(tracksResponse.data),\n    [tracksResponse]\n  )\n\n  const handleTrackPlayClick = (trackId: string) => {\n    if (!playerTracks) return\n    const playerTrackIndex = playerTracks.findIndex((track) => track.id === trackId)\n    if (playerPlaylistId !== currentPlaylistId) {\n      loadPlaylist(currentPlaylistId, playerTracks, playerTrackIndex)\n    }\n    const playerTrack = playerTracks.find((track) => track.id === trackId)\n    if (playerTrack) {\n      play(playerTrack, currentPlaylistId)\n    }\n  }\n\n  if (isLoading) return null\n\n  return (\n    <>\n      {isProfileOwner && (\n        <Button className={s.uploadTrackButton} onClick={handleOpenCreateTrackModal}>\n          {t('tracks.button.upload_track')}\n        </Button>\n      )}\n      <TracksTable\n        trackRows={\n          tracksResponse?.data?.map((track, index) => {\n            const image = getImageByType(track.attributes.images, ImageType.MEDIUM)\n            return {\n              index,\n              id: track.id,\n              title: track.attributes.title,\n              imageSrc: image?.url || noCoverPlaceholder,\n              addedAt: track.attributes.addedAt,\n              artists: ['Artist 1', 'Artist 2'],\n              duration: 100,\n              likesCount: track.attributes.likesCount,\n              dislikesCount: track.attributes.dislikesCount,\n              currentUserReaction: track.attributes.currentUserReaction,\n              url: track.attributes.attachments[0].url,\n              isPublished: track.attributes.isPublished,\n            }\n          }) ?? []\n        }\n        renderTrackRow={(trackRow) => (\n          <TrackRow\n            key={trackRow.id}\n            trackRow={trackRow}\n            onTrackPlayClick={handleTrackPlayClick}\n            renderActionsCell={() => (\n              <TrackActions\n                trackId={trackRow.id}\n                isOwner={isProfileOwner}\n                isPublished={trackRow.isPublished}\n                reaction={undefined}\n                likesCount={undefined}\n              />\n            )}\n          />\n        )}\n      />\n      {tracksResponse && (\n        <Pagination\n          page={pageNumber}\n          pagesCount={tracksResponse.meta.pagesCount || 1}\n          onPageChange={handlePageChange}\n          alwaysVisible\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/TracksTab/index.ts",
    "content": "export * from './TracksTab.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/UserTabs.tsx",
    "content": "import { useEffect } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { useSearchParams } from 'react-router'\n\nimport { useOwnerData } from '@/pages/UserPage/hooks'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/components'\n\nimport { LikedTracksTab } from './LikedTracksTab'\nimport { MyLikedPlaylistsTab } from './MyLikedPlaylistsTab'\nimport { PlaylistsTab } from './PlaylistsTab'\nimport { TracksTab } from './TracksTab/TracksTab'\nimport { UserTabsSkeleton } from './UserTabsSkeleton'\n\nexport const UserTabs = () => {\n  const { t } = useTranslation()\n  const { isProfileOwner, userLogin, isContentLoading } = useOwnerData()\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const tabFromUrl = searchParams.get('tab') || 'playlists'\n  const allowedTabs = isProfileOwner\n    ? ['playlists', 'tracks', 'liked-playlists', 'liked-tracks']\n    : ['playlists', 'tracks']\n  const activeTab = allowedTabs.includes(tabFromUrl) ? tabFromUrl : 'playlists'\n\n  useEffect(() => {\n    if (tabFromUrl === activeTab) {\n      return\n    }\n\n    setSearchParams((prev) => {\n      const next = new URLSearchParams(prev)\n      next.delete('page')\n\n      if (activeTab === 'playlists') {\n        next.delete('tab')\n      } else {\n        next.set('tab', activeTab)\n      }\n\n      return next\n    })\n  }, [activeTab, setSearchParams, tabFromUrl])\n\n  const handleTabChange = (value: string) => {\n    setSearchParams((prev) => {\n      const next = new URLSearchParams(prev)\n      next.delete('page')\n\n      if (value === 'playlists') {\n        next.delete('tab')\n      } else {\n        next.set('tab', value)\n      }\n\n      return next\n    })\n  }\n\n  if (isContentLoading) {\n    return <UserTabsSkeleton />\n  }\n\n  return (\n    <Tabs value={activeTab} onValueChange={handleTabChange}>\n      <TabsList>\n        <TabsTrigger value=\"playlists\">\n          {t('tabs.playlists')}\n          {!isProfileOwner && userLogin && ` ${userLogin}${t('tabs.possessive_case')}`}\n        </TabsTrigger>\n        <TabsTrigger value=\"tracks\">\n          {t('tabs.tracks')}\n          {!isProfileOwner && userLogin && ` ${userLogin}${t('tabs.possessive_case')}`}\n        </TabsTrigger>\n        {isProfileOwner && (\n          <>\n            <TabsTrigger value=\"liked-playlists\">{t('tabs.liked_playlists')}</TabsTrigger>\n            <TabsTrigger value=\"liked-tracks\">{t('tabs.liked_tracks')}</TabsTrigger>\n          </>\n        )}\n      </TabsList>\n      <TabsContent value=\"playlists\">\n        <PlaylistsTab />\n      </TabsContent>\n      <TabsContent value=\"tracks\">\n        <TracksTab />\n      </TabsContent>\n      {isProfileOwner && (\n        <>\n          <TabsContent value=\"liked-playlists\">\n            <MyLikedPlaylistsTab />\n          </TabsContent>\n          <TabsContent value=\"liked-tracks\">\n            <LikedTracksTab />\n          </TabsContent>\n        </>\n      )}\n    </Tabs>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/UserTabsSkeleton/UserTabsSkeleton.module.css",
    "content": ".tabs {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 32px;\n}\n\n.playlistsTab {\n  align-self: self-start;\n  display: flex;\n  gap: 8px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/UserTabsSkeleton/UserTabsSkeleton.tsx",
    "content": "import { Skeleton } from '@/shared/components'\nimport { PlaylistCardSkeleton } from '@/features/playlists'\nimport s from './UserTabsSkeleton.module.css'\nimport { USER_TABS_SKELETON_PLAYLISTS } from '@/shared/constants'\n\nexport const UserTabsSkeleton = () => {\n  return (\n    <div className={s.tabs}>\n      <Skeleton height={'45px'} />\n      <Skeleton width={'330px'} height={'55px'} />\n      <div className={s.playlistsTab}>\n        {Array.from({ length: USER_TABS_SKELETON_PLAYLISTS }).map((_el, i) => (\n          <PlaylistCardSkeleton key={i} />\n        ))}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/UserTabsSkeleton/index.ts",
    "content": "export * from './UserTabsSkeleton.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/UserTabs/index.ts",
    "content": "export * from './UserTabs'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/UserPage/ui/index.ts",
    "content": "export * from './UserInfo'\nexport * from './UserTabs'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/hooks/index.ts",
    "content": "export * from './usePageBackgroundColor.ts'\nexport * from './usePageSearchParams'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/hooks/usePageBackgroundColor.ts",
    "content": "import { useEffect, useRef, useState } from 'react'\n\nconst DEFAULT_BACKGROUND_COLOR = '#3333a3'\n\nexport const usePageBackgroundColor = (\n  url: string | null | undefined,\n  isSuccess: boolean,\n  isLocalUrlData?: boolean\n) => {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null)\n  const [dominantColor, setDominantColor] = useState<string>('')\n\n  useEffect(() => {\n    if (isSuccess && !url) {\n      setDominantColor(DEFAULT_BACKGROUND_COLOR)\n    }\n    if (url) {\n      const img = new Image()\n      img.crossOrigin = 'anonymous'\n      img.src = isLocalUrlData ? url : url + '?' //to avoid CORS error\n      img.onload = () => {\n        if (isLocalUrlData) {\n          URL.revokeObjectURL(url)\n        }\n        const canvas = canvasRef.current\n        if (canvas) {\n          canvas.width = img.naturalWidth\n          canvas.height = img.naturalHeight\n          const ctx = canvas.getContext('2d', { willReadFrequently: true })\n          ctx!.drawImage(img, 0, 0)\n\n          const step = 5\n          let red = 0,\n            green = 0,\n            blue = 0,\n            pixelsCount = 0\n          for (let y = canvas.height * 0.25; y < canvas.height / 2; y += step) {\n            for (let x = canvas.width * 0.25; x < canvas.width / 2; x += step) {\n              const data = ctx!.getImageData(x, y, 1, 1).data\n              red += data[0]\n              green += data[1]\n              blue += data[2]\n              pixelsCount++\n            }\n          }\n          const color = `rgb(${Math.round(red / pixelsCount)}, ${Math.round(green / pixelsCount)}, ${Math.round(blue / pixelsCount)})`\n          setDominantColor(color)\n        }\n      }\n    }\n  }, [url, isSuccess])\n\n  return { dominantColor, canvasRef }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/hooks/usePageSearchParams.ts",
    "content": "import { useSearchParams } from 'react-router'\n\nimport { useDebounce } from '@/shared/hooks'\n\nexport const usePageSearchParams = () => {\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const search = searchParams.get('search') || ''\n  const debouncedSearch = useDebounce(search, 500)\n\n  const sortBy = searchParams.get('sortBy') as 'addedAt' | 'likesCount'\n  const sortDirection = searchParams.get('sortDirection') as 'asc' | 'desc'\n  const tagsIds = searchParams.get('tags')?.split(',').filter(Boolean) || []\n  const artistsIds = searchParams.get('artists')?.split(',').filter(Boolean) || []\n\n  const pageNumber = Number(searchParams.get('page')) || 1\n\n  const handlePageChange = (page: number) => {\n    setSearchParams((prev) => {\n      if (page === 1) {\n        prev.delete('page')\n      } else {\n        prev.set('page', page.toString())\n      }\n      return prev\n    })\n  }\n\n  return {\n    search,\n    debouncedSearch,\n    sortBy,\n    sortDirection,\n    tagsIds,\n    artistsIds,\n    pageNumber,\n    handlePageChange,\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/index.ts",
    "content": "export * from './ui/ContentList'\nexport * from './ui/PageWithHeader'\nexport * from './ui/PageWithoutHeader'\nexport * from './ui/SearchTags'\nexport * from './ui/SearchTextField'\nexport * from './ui/SortSelect'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/ContentList/ContentList.module.css",
    "content": ".title {\n  margin-bottom: 20px;\n}\n\n.list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--list-gap, 8px);\n  padding-bottom: 8px;\n}\n\n.listRow {\n  display: flex;\n  flex-direction: column;\n  gap: 0;\n  padding: 40px 0;\n}\n\n.listRow li {\n  width: 100%;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/ContentList/ContentList.tsx",
    "content": "import clsx from 'clsx'\n\nimport { Typography } from '@/shared/components/Typography/Typography.tsx'\n\nimport s from './ContentList.module.css'\n\ntype ContentListProps<T> = {\n  title?: string\n  data: T[] | undefined\n  renderItem: (item: T) => React.ReactNode\n  listClassName?: string\n  isLoading?: boolean\n  skeleton?: React.ReactNode\n  emptyMessage?: string\n  layout?: 'column' | 'row'\n}\n\nconst SKELETON_ITEM_COUNT = 10\n\nexport const ContentList = <T,>({\n  title,\n  data = [],\n  renderItem,\n  listClassName,\n  layout = 'column',\n  isLoading,\n  skeleton,\n  emptyMessage,\n}: ContentListProps<T>) => {\n  if (data?.length === 0 && !isLoading) {\n    return <Typography variant=\"body2\">{emptyMessage}</Typography>\n  }\n\n  return (\n    <section>\n      {title && (\n        <Typography variant=\"h2\" className={s.title}>\n          {title}\n        </Typography>\n      )}\n      <ul className={clsx(s.list, layout === 'row' && s.listRow, listClassName)}>\n        {isLoading\n          ? Array.from({ length: SKELETON_ITEM_COUNT }).map((_, i) => <li key={i}>{skeleton}</li>)\n          : data.map((item, index) => <li key={index}>{renderItem(item)}</li>)}\n      </ul>\n    </section>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/ContentList/index.ts",
    "content": "export * from './ContentList.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/PageWithHeader/PageWithHeader.module.css",
    "content": ".wrapper {\n  min-height: calc(100vh - var(--header-height) - var(--player-height));\n  padding: 30px 40px;\n  background: linear-gradient(\n    180deg,\n    var(--page-gradient-color, #3333a3) 0,\n    var(--color-bg-secondary) 300px,\n    var(--color-bg-secondary) 100%\n  );\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/PageWithHeader/PageWithHeader.tsx",
    "content": "import clsx from 'clsx'\n\nimport s from './PageWithHeader.module.css'\n\ntype PageWithHeaderProps = {\n  children: React.ReactNode\n  className?: string\n}\n\nexport const PageWithHeader = ({ children, className }: PageWithHeaderProps) => {\n  return <div className={clsx(s.wrapper, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/PageWithHeader/index.ts",
    "content": "export * from './PageWithHeader.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/PageWithoutHeader/PageWithoutHeader.module.css",
    "content": ".wrapper {\n  min-height: calc(100vh - var(--player-height));\n  margin-top: calc(0px - var(--header-height));\n  padding: var(--header-height) 40px 0;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/PageWithoutHeader/PageWithoutHeader.tsx",
    "content": "import clsx from 'clsx'\n\nimport s from './PageWithoutHeader.module.css'\n\ntype PageWithoutHeaderProps = {\n  children: React.ReactNode\n  className?: string\n  backgroundColor?: string\n}\n\nexport const PageWithoutHeader = ({\n  children,\n  className,\n  backgroundColor,\n}: PageWithoutHeaderProps) => {\n  const inlineStyles = backgroundColor\n    ? { background: `linear-gradient(180deg, ${backgroundColor} 0, #141414 300px, #141414 100%)` }\n    : {}\n\n  return (\n    <div className={clsx(s.wrapper, className)} style={inlineStyles}>\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/PageWithoutHeader/index.ts",
    "content": "export * from './PageWithoutHeader.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/SearchTags/SearchTags.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { useSearchParams } from 'react-router'\n\nimport { useFindArtistsQuery } from '@/features/artists'\nimport { useFindTagsQuery } from '@/features/tags'\nimport { Autocomplete } from '@/shared/components'\nimport { useDebounce } from '@/shared/hooks'\n\ntype SearchType = 'tags' | 'artists'\n\ntype SearchTagsProps = {\n  type: SearchType\n  className?: string\n  label?: string\n  placeholder?: string\n}\n\nexport const SearchTags = ({ type, className, label, placeholder }: SearchTagsProps) => {\n  const { t } = useTranslation()\n\n  const [searchParams, setSearchParams] = useSearchParams()\n  const [searchTerm, setSearchTerm] = useState('')\n  const debouncedSearchTerm = useDebounce(searchTerm, 700)\n\n  const paramKey = type === 'tags' ? 'tags' : 'artists'\n\n  const [selectedItems, setSelectedItems] = useState<string[]>(() => {\n    const initialItems = searchParams.get(paramKey)?.split(',').filter(Boolean) || []\n    return initialItems\n  })\n\n  const { data: tagsData } = useFindTagsQuery(\n    { value: debouncedSearchTerm },\n    { skip: type !== 'tags' }\n  )\n  const { data: artistsData } = useFindArtistsQuery(debouncedSearchTerm, {\n    skip: type !== 'artists',\n  })\n\n  const data = type === 'tags' ? tagsData : artistsData\n\n  useEffect(() => {\n    const newSearchParams = new URLSearchParams(searchParams)\n\n    if (selectedItems.length > 0) {\n      newSearchParams.set(paramKey, selectedItems.join(','))\n    } else {\n      newSearchParams.delete(paramKey)\n    }\n\n    setSearchParams(newSearchParams)\n  }, [selectedItems, searchParams, setSearchParams, paramKey])\n\n  const options =\n    data?.map((item) => ({\n      label: type === 'tags' ? item.name : item.name,\n      value: item.id,\n    })) || []\n\n  const defaultLabel = type === 'tags' ? t('tags.label') : t('artists.label')\n  const defaultPlaceholder = type === 'tags' ? t('tags.placeholder') : t('artists.placeholder')\n\n  return (\n    <Autocomplete\n      options={options}\n      value={selectedItems}\n      onChange={setSelectedItems}\n      label={label || defaultLabel}\n      placeholder={placeholder || defaultPlaceholder}\n      searchTerm={searchTerm}\n      setSearchTerm={setSearchTerm}\n      className={className}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/SearchTags/index.ts",
    "content": "export * from './SearchTags.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/SearchTextField/SearchTextField.tsx",
    "content": "import { useSearchParams } from 'react-router'\n\nimport { TextField, type TextFieldProps } from '@/shared/components'\nimport { SearchIcon } from '@/shared/icons'\n\nexport const SearchTextField = (props: Omit<TextFieldProps, 'icon' | 'inputSize'>) => {\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setSearchParams((prev) => {\n      if (e.target.value === '') {\n        prev.delete('search')\n      } else {\n        prev.set('search', e.target.value)\n        prev.set('page', '1')\n      }\n\n      return prev\n    })\n  }\n\n  const search = searchParams.get('search') || ''\n\n  return (\n    <TextField\n      {...props}\n      value={search}\n      onChange={handleChange}\n      icon={<SearchIcon width={20} height={20} />}\n      inputSize=\"l\"\n      autoComplete=\"off\"\n    />\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/SearchTextField/index.ts",
    "content": "export * from './SearchTextField.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/SortSelect/SortSelect.module.css",
    "content": ".selectLabel {\n  display: flex;\n  flex-shrink: 0;\n  gap: 8px;\n  align-items: center;\n\n  min-width: 210px;\n\n  white-space: nowrap;\n}\n\n.select {\n  flex-shrink: 1;\n  min-width: 145px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/SortSelect/SortSelect.tsx",
    "content": "import { t } from 'i18next'\nimport { useSearchParams } from 'react-router'\n\nimport { Select, type SelectProps } from '@/shared/components'\n\nimport s from './SortSelect.module.css'\n\nexport const SortSelect = (props: Omit<SelectProps, 'options'>) => {\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const currentSortBy = searchParams.get('sortBy') || 'addedAt'\n  const currentSortDirection = searchParams.get('sortDirection') || 'desc'\n  const currentValue = `${currentSortBy}_${currentSortDirection}`\n\n  const handleSortChange = (event: React.ChangeEvent<HTMLSelectElement>) => {\n    const [sortBy, sortDirection] = event.target.value.split('_')\n\n    setSearchParams((prev) => {\n      prev.set('sortBy', sortBy)\n      prev.set('sortDirection', sortDirection)\n      return prev\n    })\n  }\n\n  return (\n    <label className={s.selectLabel}>\n      <span>{t('sort.label')}</span>\n      <Select\n        {...props}\n        value={currentValue}\n        onChange={handleSortChange}\n        options={[\n          { value: 'addedAt_desc', label: t('sort.newest_first') },\n          { value: 'addedAt_asc', label: t('sort.oldest_first') },\n          { value: 'likesCount_desc', label: t('sort.most_liked') },\n          { value: 'likesCount_asc', label: t('sort.least_liked') },\n        ]}\n        className={s.select}\n      />\n    </label>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/pages/common/ui/SortSelect/index.ts",
    "content": "export * from './SortSelect.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/pages/index.ts",
    "content": "export * from './MainPage'\nexport * from './PlaylistPage'\nexport * from './PlaylistsPage'\nexport * from './TrackPage'\nexport * from './TracksPage'\nexport * from './UserPage'\n"
  },
  {
    "path": "apps/rtk-query/src/player/MIGRATION_GUIDE.md",
    "content": "# Player API Migration Guide\n\nThe player has been refactored to be independent of RTK Query cache. It now stores full track information internally, making it more reliable and decoupled from the API layer.\n\n## Breaking Changes\n\n### 1. Actions Now Accept Full Track Objects\n\n**Before:**\n\n```typescript\n// Old API - just IDs\ndispatch(playTrack({ trackId: '123' }))\ndispatch(loadPlaylist({ playlistId: 'playlist-1', trackIds: ['1', '2', '3'] }))\ndispatch(addToQueue(['track-1', 'track-2']))\ndispatch(insertNext('track-3'))\n```\n\n**After:**\n\n```typescript\n// New API - full track objects\nimport type { Track } from '@/player'\n\nconst track: Track = {\n  id: '123',\n  title: 'Song Title',\n  artist: 'Artist Name',\n  url: 'https://example.com/audio.mp3', // Required!\n  duration: 180, // in seconds\n  albumArt: 'https://example.com/cover.jpg', // optional\n  album: 'Album Name', // optional\n  artistId: 'artist-1', // optional\n  albumId: 'album-1', // optional\n}\n\ndispatch(playTrack({ track }))\n\n// With playlist context\nconst tracks: Track[] = [\n  /* array of track objects */\n]\ndispatch(\n  playTrack({\n    track,\n    playlistId: 'playlist-1',\n    tracks, // All tracks in the playlist\n  })\n)\n\n// Load entire playlist\ndispatch(\n  loadPlaylist({\n    playlistId: 'playlist-1',\n    tracks: tracks, // Array of Track objects\n    startIndex: 0, // Optional\n  })\n)\n\n// Queue operations\ndispatch(addToQueue(tracks))\ndispatch(insertNext(track))\n```\n\n### 2. Hooks Signature Changes\n\n**Before:**\n\n```typescript\nconst { play } = usePlayerControls()\nplay('track-123')\n\nconst trackPlayer = useTrackPlayer('track-123')\n\nconst { loadPlaylist } = useQueueControls()\nloadPlaylist('playlist-1', ['track-1', 'track-2'])\n```\n\n**After:**\n\n```typescript\nimport type { Track } from '@/player'\n\nconst { play } = usePlayerControls()\nplay(track, 'playlist-1', tracks)\n\nconst trackPlayer = useTrackPlayer(track) // Pass full track object\n\nconst { loadPlaylist } = useQueueControls()\nloadPlaylist('playlist-1', tracks) // Array of Track objects\n```\n\n## How to Convert Your API Data to Track Objects\n\n### Example: Converting from RTK Query Response\n\n```typescript\nimport { useFetchTracksQuery } from '@/features/tracks/api'\nimport { usePlayerControls } from '@/player'\nimport type { Track } from '@/player'\n\nfunction TracksList() {\n  const { data } = useFetchTracksQuery()\n  const { play } = usePlayerControls()\n\n  // Helper function to convert API track to Player track\n  const convertToPlayerTrack = (apiTrack: any): Track => ({\n    id: apiTrack.id,\n    title: apiTrack.attributes.title,\n    artist: apiTrack.attributes.artists?.[0]?.name || 'Unknown Artist',\n    album: apiTrack.attributes.album?.name,\n    duration: apiTrack.attributes.duration,\n    url: apiTrack.attributes.attachments?.[0]?.url, // Audio file URL\n    albumArt: apiTrack.attributes.images?.medium || apiTrack.attributes.images?.small,\n    artistId: apiTrack.attributes.artists?.[0]?.id,\n  })\n\n  const handlePlay = (apiTrack: any) => {\n    const track = convertToPlayerTrack(apiTrack)\n\n    // Option 1: Play single track\n    play(track)\n\n    // Option 2: Play with playlist context\n    const allTracks = data?.data.map(convertToPlayerTrack) || []\n    play(track, 'all-tracks', allTracks)\n  }\n\n  return (\n    <div>\n      {data?.data.map(apiTrack => (\n        <div key={apiTrack.id}>\n          <button onClick={() => handlePlay(apiTrack)}>Play</button>\n          <span>{apiTrack.attributes.title}</span>\n        </div>\n      ))}\n    </div>\n  )\n}\n```\n\n### Example: Load Playlist\n\n```typescript\nimport { useFetchPlaylistTracksQuery } from '@/features/tracks/api'\nimport { useQueueControls } from '@/player'\nimport type { Track } from '@/player'\n\nfunction PlaylistView({ playlistId }: { playlistId: string }) {\n  const { data } = useFetchPlaylistTracksQuery({ playlistId })\n  const { loadPlaylist } = useQueueControls()\n\n  const handlePlayAll = () => {\n    if (!data?.data) return\n\n    const tracks: Track[] = data.data.map(apiTrack => ({\n      id: apiTrack.id,\n      title: apiTrack.attributes.title,\n      artist: apiTrack.attributes.artists?.[0]?.name || 'Unknown',\n      url: apiTrack.attributes.attachments?.[0]?.url,\n      duration: apiTrack.attributes.duration,\n      albumArt: apiTrack.attributes.images?.small,\n    }))\n\n    loadPlaylist(playlistId, tracks, 0)\n  }\n\n  return (\n    <div>\n      <button onClick={handlePlayAll}>Play All</button>\n    </div>\n  )\n}\n```\n\n### Example: Using useTrackPlayer Hook\n\n```typescript\nimport { useTrackPlayer } from '@/player'\nimport type { Track } from '@/player'\n\n// Create track object from your API data\nfunction TrackCard({ apiTrack }: { apiTrack: any }) {\n  const track: Track = {\n    id: apiTrack.id,\n    title: apiTrack.attributes.title,\n    artist: apiTrack.attributes.artists?.[0]?.name || 'Unknown',\n    url: apiTrack.attributes.attachments?.[0]?.url,\n    duration: apiTrack.attributes.duration,\n    albumArt: apiTrack.attributes.images?.small,\n  }\n\n  const { isPlaying, togglePlayPause, progress } = useTrackPlayer(track)\n\n  return (\n    <div>\n      <button onClick={togglePlayPause}>\n        {isPlaying ? 'Pause' : 'Play'}\n      </button>\n      <h3>{track.title}</h3>\n      {isPlaying && <ProgressBar progress={progress} />}\n    </div>\n  )\n}\n```\n\n## Track Type Definition\n\n```typescript\ninterface Track {\n  id: string // Required - unique track identifier\n  title: string // Required - track title\n  artist: string // Required - artist name\n  url: string // Required - audio file URL (THIS IS CRITICAL!)\n  duration: number // Required - duration in seconds\n\n  // Optional fields\n  album?: string // Album name\n  albumArt?: string // Cover image URL\n  artistId?: string // Artist ID for navigation\n  albumId?: string // Album ID for navigation\n}\n```\n\n## Important Notes\n\n1. **`url` field is REQUIRED**: The player needs the audio file URL to play tracks. Make sure your track objects include this.\n\n2. **Track data is stored in player state**: Once you pass tracks to the player, they're stored internally. The player no longer depends on RTK Query cache.\n\n3. **Performance**: The player uses normalized storage (tracks by ID) for optimal performance.\n\n4. **Queue operations**: When you add tracks to queue or load playlists, all track data must be provided upfront.\n\n## Migration Checklist\n\n- [ ] Update all `playTrack` calls to pass full `Track` object\n- [ ] Update all `loadPlaylist` calls to pass `Track[]` instead of `string[]`\n- [ ] Update all `addToQueue` calls to pass `Track[]`\n- [ ] Update all `insertNext` calls to pass `Track` object\n- [ ] Update `useTrackPlayer` to pass full `Track` object\n- [ ] Create helper functions to convert your API responses to `Track` objects\n- [ ] Ensure all tracks have valid `url` fields pointing to audio files\n- [ ] Test playback with the new API\n\n## Files to Update in Your Codebase\n\nBased on the build errors, update these files:\n\n1. **src/features/tracks/ui/TrackInfoCell/TrackInfoCell.tsx** (line 29)\n\n   - Update to pass full track object with URL\n\n2. **src/pages/TracksPage/TracksPage.tsx** (line 38)\n   - Update loadPlaylist call to pass track objects array\n\n## Need Help?\n\nIf you're unsure about any part of the migration, check:\n\n- `src/player/README.md` - Usage examples\n- `src/player/SPECIFICATION.md` - Complete technical specification\n- `src/player/types/player.types.ts` - Type definitions\n"
  },
  {
    "path": "apps/rtk-query/src/player/README.md",
    "content": "# Music Player - Business Logic\n\nThis folder contains the complete business logic for the music player, implemented with Redux Toolkit.\n\n## Setup\n\n### 1. Add to Redux Store\n\nUpdate your store configuration to include the player slice and middleware:\n\n```typescript\n// src/app/store/store.ts\nimport { configureStore } from '@reduxjs/toolkit'\nimport { playerSlice, playerMiddleware } from '@/player'\n\nexport const store = configureStore({\n  reducer: {\n    // ... other reducers\n    [playerSlice.name]: playerSlice.reducer,\n  },\n  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(playerMiddleware),\n  // ... other middleware\n})\n```\n\n### 2. Basic Usage in Components\n\n#### Simple Track Item Component\n\n```tsx\nimport { useTrackPlayer } from '@/player'\n\nfunction TrackItem({ trackId }: { trackId: string }) {\n  const { isPlaying, isPaused, isCurrentTrack, progress, togglePlayPause } = useTrackPlayer(trackId)\n\n  return (\n    <div className=\"track-item\">\n      <button onClick={togglePlayPause}>{isPlaying ? '⏸' : '▶'}</button>\n\n      {/* Only show progress bar for current track */}\n      {isCurrentTrack && (\n        <div className=\"progress-bar\">\n          <div style={{ width: `${progress}%` }} />\n        </div>\n      )}\n    </div>\n  )\n}\n```\n\n#### Player Controls Component\n\n```tsx\nimport { usePlayerControls, usePlaybackState, useCurrentTrack, useTrackNavigation } from '@/player'\n\nfunction PlayerControls() {\n  const { togglePlayPause } = usePlayerControls()\n  const { isPlaying } = usePlaybackState()\n  const { track } = useCurrentTrack()\n  const { next, previous, hasNext, hasPrevious } = useTrackNavigation()\n\n  return (\n    <div className=\"player-controls\">\n      <button onClick={previous} disabled={!hasPrevious}>\n        ⏮\n      </button>\n      <button onClick={togglePlayPause}>{isPlaying ? '⏸' : '▶'}</button>\n      <button onClick={next} disabled={!hasNext}>\n        ⏭\n      </button>\n\n      {track && (\n        <div className=\"now-playing\">\n          <span>{track.attributes.title}</span>\n        </div>\n      )}\n    </div>\n  )\n}\n```\n\n#### Progress Bar Component\n\n```tsx\nimport { usePlaybackProgress, usePlayerControls } from '@/player'\n\nfunction ProgressBar() {\n  const { currentTime, duration, progress, formattedTime } = usePlaybackProgress()\n  const { seek } = usePlayerControls()\n\n  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    const rect = e.currentTarget.getBoundingClientRect()\n    const x = e.clientX - rect.left\n    const percentage = x / rect.width\n    seek(percentage * duration)\n  }\n\n  return (\n    <div className=\"progress-container\">\n      <span>{formattedTime.current}</span>\n      <div className=\"progress-bar\" onClick={handleClick}>\n        <div style={{ width: `${progress}%` }} />\n      </div>\n      <span>{formattedTime.duration}</span>\n    </div>\n  )\n}\n```\n\n#### Volume Control Component\n\n```tsx\nimport { useVolumeControl } from '@/player'\n\nfunction VolumeControl() {\n  const { volume, isMuted, volumePercentage, setVolume, toggleMute } = useVolumeControl()\n\n  return (\n    <div className=\"volume-control\">\n      <button onClick={toggleMute}>{isMuted ? '🔇' : '🔊'}</button>\n      <input\n        type=\"range\"\n        min=\"0\"\n        max=\"100\"\n        value={volumePercentage}\n        onChange={(e) => setVolume(Number(e.target.value) / 100)}\n      />\n    </div>\n  )\n}\n```\n\n#### Playback Modes Component\n\n```tsx\nimport { usePlaybackModes } from '@/player'\n\nfunction PlaybackModes() {\n  const { repeatMode, shuffleMode, cycleRepeatMode, toggleShuffle } = usePlaybackModes()\n\n  return (\n    <div className=\"playback-modes\">\n      <button onClick={toggleShuffle} className={shuffleMode ? 'active' : ''}>\n        🔀\n      </button>\n\n      <button onClick={cycleRepeatMode}>\n        {repeatMode === 'one' ? '🔂' : repeatMode === 'all' ? '🔁' : '↻'}\n      </button>\n    </div>\n  )\n}\n```\n\n#### Playlist Component\n\n```tsx\nimport { useQueueControls } from '@/player'\n\nfunction PlaylistView({ playlistId, trackIds }: Props) {\n  const { loadPlaylist } = useQueueControls()\n\n  const handlePlayAll = () => {\n    loadPlaylist(playlistId, trackIds, 0)\n  }\n\n  return (\n    <div>\n      <button onClick={handlePlayAll}>Play All</button>\n\n      <div className=\"track-list\">\n        {trackIds.map((trackId) => (\n          <TrackItem key={trackId} trackId={trackId} />\n        ))}\n      </div>\n    </div>\n  )\n}\n```\n\n## Available Hooks\n\n### Core Hooks\n\n- **`usePlayer()`** - All-in-one hook with complete player functionality\n- **`usePlayerControls()`** - Play, pause, stop, seek, navigation controls\n- **`usePlaybackState()`** - Current playback state (playing, paused, loading, etc.)\n- **`useCurrentTrack()`** - Current playing track information\n- **`usePlaybackProgress()`** - Time, duration, progress percentage\n\n### Track-Specific Hooks (Performance Optimized)\n\n- **`useTrackPlayer(trackId)`** - Complete player state and controls for specific track\n- **`useTrackPlaybackState(trackId)`** - Playback state for specific track\n- **`useTrackProgress(trackId)`** - Progress for specific track (only if current)\n- **`useIsCurrentTrack(trackId)`** - Check if track is currently playing\n- **`useTrackQueuePosition(trackId)`** - Track's position in queue\n\n### Feature-Specific Hooks\n\n- **`useVolumeControl()`** - Volume and mute controls\n- **`usePlayerQueue()`** - Queue state and manipulation\n- **`usePlaybackModes()`** - Repeat and shuffle modes\n- **`useTrackNavigation()`** - Next/previous track navigation\n- **`usePlayerKeyboardControls()`** - Enable keyboard shortcuts\n\n## Actions\n\n### Playback Control\n\n```typescript\ndispatch(playTrack({ trackId, playlistId?, queue? }))\ndispatch(pause())\ndispatch(resume())\ndispatch(stop())\ndispatch(togglePlayPause())\n```\n\n### Navigation\n\n```typescript\ndispatch(nextTrack())\ndispatch(previousTrack())\ndispatch(playTrackAtIndex(index))\n```\n\n### Progress\n\n```typescript\ndispatch(seek(timeInSeconds))\n```\n\n### Volume\n\n```typescript\ndispatch(setVolume(0.5)) // 0-1\ndispatch(toggleMute())\n```\n\n### Modes\n\n```typescript\ndispatch(setRepeatMode('off' | 'one' | 'all'))\ndispatch(toggleShuffle())\n```\n\n### Queue\n\n```typescript\ndispatch(loadPlaylist({ playlistId, trackIds, startIndex? }))\ndispatch(addToQueue(['trackId1', 'trackId2']))\ndispatch(insertNext('trackId'))\ndispatch(removeFromQueue(index))\ndispatch(clearQueue())\n```\n\n## Selectors\n\nAll selectors are memoized for optimal performance. Use selector factories for track-specific state:\n\n```typescript\nimport { makeSelectTrackPlaybackState } from '@/player'\n\n// In component:\nconst selectTrackState = useMemo(makeSelectTrackPlaybackState, [])\nconst trackState = useSelector((state) => selectTrackState(state, trackId))\n```\n\n## Performance Considerations\n\n### Track List Optimization\n\nWhen rendering lists of tracks, use track-specific hooks to prevent unnecessary rerenders:\n\n```tsx\n// ✅ Good - only rerenders when this track's state changes\nfunction TrackItem({ trackId }) {\n  const { isPlaying } = useTrackPlaybackState(trackId)\n  // ...\n}\n\n// ❌ Bad - rerenders on any player state change\nfunction TrackItem({ trackId }) {\n  const currentTrackId = useSelector(selectCurrentTrackId)\n  const isPlaying = currentTrackId === trackId\n  // ...\n}\n```\n\n### Component Memoization\n\nWrap track components in `React.memo`:\n\n```tsx\nexport default React.memo(TrackItem)\n```\n\n## Keyboard Shortcuts\n\nEnable keyboard controls in your main App component:\n\n```tsx\nimport { usePlayerKeyboardControls } from '@/player'\n\nfunction App() {\n  usePlayerKeyboardControls(true)\n\n  return <YourApp />\n}\n```\n\n**Available shortcuts:**\n\n- `Space` - Play/Pause\n- `Arrow Right` - Seek forward 5s\n- `Arrow Left` - Seek backward 5s\n- `Arrow Up` - Volume up\n- `Arrow Down` - Volume down\n- `M` - Toggle mute\n- `N` - Next track\n- `P` - Previous track\n\n## File Structure\n\n```\nsrc/player/\n├── index.ts                 # Main exports\n├── player.ts                # Audio instance\n├── playerSlice.ts          # Redux slice\n├── playerMiddleware.ts     # Audio sync middleware\n├── playerSelectors.ts      # Memoized selectors\n├── playerHooks.ts          # Custom React hooks\n├── types/\n│   └── player.types.ts     # TypeScript types\n└── utils/\n    ├── shuffle.ts          # Shuffle algorithms\n    ├── format-time.ts       # Time formatting\n    ├── throttle.ts         # Throttle/debounce\n    └── index.ts            # Utils exports\n```\n\n## Additional Features\n\n### Persistence\n\nThe player automatically persists:\n\n- Volume level\n- Repeat mode\n- Shuffle mode\n\nTo localStorage for session recovery.\n\n### Error Handling\n\nErrors are automatically captured and stored in state:\n\n```tsx\nconst { error } = usePlaybackState()\n\nif (error) {\n  return <ErrorMessage message={error} />\n}\n```\n\n## Next Steps\n\n1. Integrate player slice into your Redux store\n2. Add player middleware\n3. Create player UI components using the provided hooks\n4. Customize styling to match your design\n5. Add keyboard shortcuts support\n6. Implement Media Session API for OS-level controls (see SPECIFICATION.md)\n\nFor complete implementation details, see [SPECIFICATION.md](./SPECIFICATION.md).\n"
  },
  {
    "path": "apps/rtk-query/src/player/SPECIFICATION.md",
    "content": "# Music Player Redux Slice - Technical Specification\n\n## 1. Overview\n\nThis specification defines a Redux Toolkit slice that wraps the HTML5 Audio API to create a fully-featured music player with playlist support, multiple playback modes, and performance-optimized state management.\n\n## 2. State Structure\n\n```typescript\ninterface Track {\n  id: string\n  title: string\n  artist: string\n  duration: number // in seconds\n  url: string\n  albumArt?: string\n}\n\ninterface Playlist {\n  id: string\n  name: string\n  trackIds: string[]\n}\n\ninterface PlayerState {\n  // Current playback state\n  currentTrackId: string | null\n  currentPlaylistId: string | null\n  playbackState: 'idle' | 'playing' | 'paused' | 'loading' | 'error'\n\n  // Playback position\n  currentTime: number // in seconds\n  duration: number // in seconds\n  buffered: number // percentage 0-100\n\n  // Volume control\n  volume: number // 0-1\n  isMuted: boolean\n\n  // Playback modes\n  repeatMode: 'off' | 'one' | 'all'\n  shuffleMode: boolean\n\n  // Queue management\n  queue: string[] // ordered track IDs\n  originalQueue: string[] // original order before shuffle\n  queueIndex: number\n\n  // Error handling\n  error: string | null\n\n  // Additional metadata\n  isLoadingTrack: boolean\n  hasNextTrack: boolean\n  hasPreviousTrack: boolean\n}\n```\n\n## 3. Core Features\n\n### 3.1 Playback Control\n\n#### Play\n\n- **Behavior**: Start playing the specified track or resume current track\n- **Rules**:\n  - If a different track is requested, stop current track and start new one from beginning\n  - If same track is requested while paused, resume from current position\n  - Only one track can play at a time\n  - Update playback state to 'loading' then 'playing'\n\n#### Pause\n\n- **Behavior**: Pause current track without resetting position\n- **Rules**:\n  - Can only pause if a track is currently playing\n  - Preserve currentTime for resume\n  - Update playback state to 'paused'\n\n### 3.2 Track Navigation\n\n#### Next Track\n\n- **Behavior**: Play the next track in queue\n- **Rules**:\n  - If repeatMode is 'one', replay current track from beginning\n  - If shuffleMode is on, use shuffled queue order\n  - If at end of queue:\n    - If repeatMode is 'all', go to first track\n    - If repeatMode is 'off', stop playback\n  - Start new track from beginning\n\n#### Previous Track\n\n- **Behavior**: Play the previous track or restart current track\n- **Rules**:\n  - If currentTime > 3 seconds, restart current track from beginning\n  - Otherwise, go to previous track in queue\n  - If at beginning of queue:\n    - If repeatMode is 'all', go to last track\n    - If repeatMode is 'off', restart current track\n\n### 3.3 Progress Tracking\n\n#### Seek\n\n- **Behavior**: Jump to specific position in current track\n- **Rules**:\n  - Validate position is within track duration\n  - Update currentTime\n  - Maintain current playback state (playing/paused)\n\n#### Time Update\n\n- **Behavior**: Sync Redux state with Audio element time\n- **Rules**:\n  - Throttled updates (max 1 per second to avoid excessive rerenders)\n  - Update currentTime, buffered percentage\n  - Check for track end condition\n\n### 3.4 Volume Control\n\n#### Set Volume\n\n- **Behavior**: Adjust playback volume\n- **Rules**:\n  - Clamp value between 0 and 1\n  - Persist to localStorage\n  - If volume > 0, unmute automatically\n\n#### Toggle Mute\n\n- **Behavior**: Mute/unmute audio\n- **Rules**:\n  - Preserve volume level when muting\n  - Restore previous volume when unmuting\n\n### 3.5 Playback Modes\n\n#### Repeat Modes\n\n- **off**: Play queue once and stop\n- **one**: Repeat current track indefinitely\n- **all**: Loop entire queue continuously\n\n#### Shuffle Mode\n\n- **Behavior**: Randomize playback order\n- **Rules**:\n  - Store original queue order in originalQueue\n  - Generate shuffled queue using Fisher-Yates algorithm\n  - Maintain current track position when toggling\n  - When shuffle is disabled, restore original order\n\n### 3.6 Queue Management\n\n#### Load Playlist\n\n- **Behavior**: Load tracks from playlist into queue\n- **Rules**:\n  - Replace current queue\n  - Reset queue index to 0\n  - Store playlist ID for reference\n  - Apply shuffle if enabled\n  - Start playing first track\n\n#### Add to Queue\n\n- **Behavior**: Append track(s) to current queue\n- **Rules**:\n  - Add to end of queue\n  - Update originalQueue if shuffle is off\n  - Don't interrupt current playback\n\n#### Insert Next\n\n- **Behavior**: Add track to play after current track\n- **Rules**:\n  - Insert at queueIndex + 1\n  - Don't interrupt current playback\n\n#### Remove from Queue\n\n- **Behavior**: Remove track from queue\n- **Rules**:\n  - Adjust queueIndex if necessary\n  - If removing current track, skip to next\n\n## 4. Redux Actions\n\n```typescript\n// Playback control\nplayTrack(trackId: string, playlistId?: string)\npause()\nresume()\nstop()\ntogglePlayPause()\n\n// Navigation\nnextTrack()\npreviousTrack()\nplayTrackAtIndex(index: number)\n\n// Progress\nseek(time: number)\nupdateTime(time: number) // Internal, called by audio events\nupdateBuffered(percentage: number)\n\n// Volume\nsetVolume(volume: number)\ntoggleMute()\n\n// Modes\nsetRepeatMode(mode: 'off' | 'one' | 'all')\ntoggleShuffle()\n\n// Queue\nloadPlaylist(playlistId: string, trackIds: string[], startIndex?: number)\naddToQueue(trackIds: string[])\ninsertNext(trackId: string)\nremoveFromQueue(index: number)\nclearQueue()\n\n// Error handling\nsetError(error: string)\nclearError()\n\n// Metadata\nsetDuration(duration: number)\nsetLoadingState(isLoading: boolean)\n```\n\n## 5. Selectors (Performance Optimized)\n\n### 5.1 Basic Selectors\n\n```typescript\n// Simple state selectors\nselectCurrentTrackId\nselectPlaybackState\nselectCurrentTime\nselectDuration\nselectVolume\nselectIsMuted\nselectRepeatMode\nselectShuffleMode\nselectQueue\nselectQueueIndex\nselectError\n```\n\n### 5.2 Computed Selectors\n\n```typescript\n// Memoized with createSelector\nselectIsPlaying // playbackState === 'playing'\nselectIsPaused // playbackState === 'paused'\nselectProgress // (currentTime / duration) * 100\nselectFormattedTime // { current: 'MM:SS', duration: 'MM:SS' }\nselectHasNextTrack\nselectHasPreviousTrack\nselectCurrentTrack // full track object from tracks entity\nselectQueueTracks // full track objects in queue order\n```\n\n### 5.3 Track-Specific Selectors (Performance Critical)\n\n**Problem**: With hundreds of tracks on page, we need to avoid rerendering all track components when only one track's state changes.\n\n**Solution**: Create parameterized selectors that only return data for specific tracks.\n\n```typescript\n// Factory function that creates a memoized selector for a specific track\nconst makeSelectTrackPlaybackState = () =>\n  createSelector(\n    [selectCurrentTrackId, selectPlaybackState, (state: RootState, trackId: string) => trackId],\n    (currentTrackId, playbackState, trackId) => ({\n      isCurrentTrack: currentTrackId === trackId,\n      isPlaying: currentTrackId === trackId && playbackState === 'playing',\n      isPaused: currentTrackId === trackId && playbackState === 'paused',\n      playbackState: currentTrackId === trackId ? playbackState : 'idle',\n    })\n  )\n\n// Usage in component: only rerenders when THIS track's state changes\nconst useTrackPlaybackState = (trackId: string) => {\n  const selectTrackPlaybackState = useMemo(makeSelectTrackPlaybackState, [])\n  return useSelector((state) => selectTrackPlaybackState(state, trackId))\n}\n```\n\n### 5.4 Progress Selector (for current track only)\n\n```typescript\nconst selectTrackProgress = createSelector(\n  [\n    selectCurrentTrackId,\n    selectCurrentTime,\n    selectDuration,\n    (state: RootState, trackId: string) => trackId,\n  ],\n  (currentTrackId, currentTime, duration, trackId) => {\n    if (currentTrackId !== trackId) return { progress: 0, currentTime: 0 }\n    return {\n      progress: duration > 0 ? (currentTime / duration) * 100 : 0,\n      currentTime,\n    }\n  }\n)\n```\n\n## 6. Audio Integration\n\n### 6.1 Audio Event Listeners\n\nSetup in middleware:\n\n```typescript\naudio.addEventListener('loadedmetadata', () => {\n  dispatch(setDuration(audio.duration))\n})\n\naudio.addEventListener(\n  'timeupdate',\n  throttle(() => {\n    dispatch(updateTime(audio.currentTime))\n  }, 1000)\n)\n\naudio.addEventListener('ended', () => {\n  dispatch(handleTrackEnded())\n})\n\naudio.addEventListener('error', (e) => {\n  dispatch(setError(audio.error?.message || 'Playback error'))\n})\n\naudio.addEventListener('loadstart', () => {\n  dispatch(setLoadingState(true))\n})\n\naudio.addEventListener('canplay', () => {\n  dispatch(setLoadingState(false))\n})\n\naudio.addEventListener('progress', () => {\n  if (audio.buffered.length > 0) {\n    const buffered = (audio.buffered.end(0) / audio.duration) * 100\n    dispatch(updateBuffered(buffered))\n  }\n})\n```\n\n### 6.2 Middleware for Audio Sync\n\nCreate Redux middleware to sync actions with Audio API:\n\n```typescript\nconst playerMiddleware = (store) => (next) => (action) => {\n  const result = next(action)\n  const state = store.getState()\n\n  switch (action.type) {\n    case 'player/playTrack': {\n      const track = selectTrackById(state, action.payload.trackId)\n      if (track) {\n        audio.src = track.url\n        audio.currentTime = 0\n        audio.play().catch((e) => {\n          store.dispatch(setError(e.message))\n        })\n      }\n      break\n    }\n\n    case 'player/pause':\n      audio.pause()\n      break\n\n    case 'player/resume':\n      audio.play().catch((e) => {\n        store.dispatch(setError(e.message))\n      })\n      break\n\n    case 'player/seek':\n      audio.currentTime = action.payload\n      break\n\n    case 'player/setVolume':\n      audio.volume = action.payload\n      break\n\n    case 'player/toggleMute':\n      audio.muted = !audio.muted\n      break\n  }\n\n  return result\n}\n```\n\n## 7. Component Integration Examples\n\n### 7.1 Track Item Component (in list of hundreds)\n\n```typescript\ninterface TrackItemProps {\n  track: Track;\n}\n\nconst TrackItem: React.FC<TrackItemProps> = ({ track }) => {\n  // This selector only causes rerender when THIS track's state changes\n  const { isPlaying, isPaused } = useTrackPlaybackState(track.id);\n  const { progress, currentTime } = useTrackProgress(track.id);\n  const dispatch = useDispatch();\n\n  const handlePlay = () => {\n    if (isPlaying) {\n      dispatch(pause());\n    } else if (isPaused) {\n      dispatch(resume());\n    } else {\n      dispatch(playTrack(track.id));\n    }\n  };\n\n  return (\n    <div className=\"track-item\">\n      <button onClick={handlePlay}>\n        {isPlaying ? '⏸' : '▶'}\n      </button>\n      <span>{track.title}</span>\n      {(isPlaying || isPaused) && (\n        <ProgressBar progress={progress} currentTime={currentTime} />\n      )}\n    </div>\n  );\n};\n\n// React.memo prevents rerenders when props don't change\nexport default React.memo(TrackItem);\n```\n\n### 7.2 Player Controls Component\n\n```typescript\nconst PlayerControls: React.FC = () => {\n  const dispatch = useDispatch();\n  const isPlaying = useSelector(selectIsPlaying);\n  const currentTrack = useSelector(selectCurrentTrack);\n  const volume = useSelector(selectVolume);\n  const isMuted = useSelector(selectIsMuted);\n  const repeatMode = useSelector(selectRepeatMode);\n  const shuffleMode = useSelector(selectShuffleMode);\n  const hasNext = useSelector(selectHasNextTrack);\n  const hasPrevious = useSelector(selectHasPreviousTrack);\n\n  return (\n    <div className=\"player-controls\">\n      <button\n        onClick={() => dispatch(previousTrack())}\n        disabled={!hasPrevious}\n      >\n        ⏮\n      </button>\n\n      <button onClick={() => dispatch(togglePlayPause())}>\n        {isPlaying ? '⏸' : '▶'}\n      </button>\n\n      <button\n        onClick={() => dispatch(nextTrack())}\n        disabled={!hasNext}\n      >\n        ⏭\n      </button>\n\n      <button\n        onClick={() => dispatch(toggleShuffle())}\n        className={shuffleMode ? 'active' : ''}\n      >\n        🔀\n      </button>\n\n      <button onClick={() => dispatch(setRepeatMode(\n        repeatMode === 'off' ? 'all' : repeatMode === 'all' ? 'one' : 'off'\n      ))}>\n        {repeatMode === 'one' ? '🔂' : repeatMode === 'all' ? '🔁' : '↻'}\n      </button>\n\n      <VolumeControl\n        volume={volume}\n        isMuted={isMuted}\n        onVolumeChange={(v) => dispatch(setVolume(v))}\n        onToggleMute={() => dispatch(toggleMute())}\n      />\n\n      {currentTrack && (\n        <div className=\"now-playing\">\n          <img src={currentTrack.albumArt} alt=\"\" />\n          <div>\n            <div>{currentTrack.title}</div>\n            <div>{currentTrack.artist}</div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n```\n\n### 7.3 Progress Bar Component\n\n```typescript\nconst ProgressBar: React.FC = () => {\n  const dispatch = useDispatch();\n  const currentTime = useSelector(selectCurrentTime);\n  const duration = useSelector(selectDuration);\n  const progress = useSelector(selectProgress);\n  const buffered = useSelector(state => state.player.buffered);\n\n  const handleSeek = (e: React.MouseEvent<HTMLDivElement>) => {\n    const rect = e.currentTarget.getBoundingClientRect();\n    const x = e.clientX - rect.left;\n    const percentage = x / rect.width;\n    const time = percentage * duration;\n    dispatch(seek(time));\n  };\n\n  return (\n    <div className=\"progress-bar-container\">\n      <span>{formatTime(currentTime)}</span>\n\n      <div className=\"progress-bar\" onClick={handleSeek}>\n        <div\n          className=\"buffered-bar\"\n          style={{ width: `${buffered}%` }}\n        />\n        <div\n          className=\"progress-bar-fill\"\n          style={{ width: `${progress}%` }}\n        />\n      </div>\n\n      <span>{formatTime(duration)}</span>\n    </div>\n  );\n};\n```\n\n### 7.4 Playlist Component\n\n```typescript\nconst PlaylistView: React.FC<{ playlistId: string }> = ({ playlistId }) => {\n  const dispatch = useDispatch();\n  const playlist = useSelector(state =>\n    selectPlaylistById(state, playlistId)\n  );\n  const tracks = useSelector(state =>\n    selectTracksByIds(state, playlist.trackIds)\n  );\n\n  const handlePlayPlaylist = () => {\n    dispatch(loadPlaylist(playlistId, playlist.trackIds, 0));\n  };\n\n  return (\n    <div>\n      <h2>{playlist.name}</h2>\n      <button onClick={handlePlayPlaylist}>Play All</button>\n\n      <div className=\"track-list\">\n        {tracks.map(track => (\n          <TrackItem key={track.id} track={track} />\n        ))}\n      </div>\n    </div>\n  );\n};\n```\n\n## 8. Performance Optimizations\n\n### 8.1 Selector Memoization\n\n- Use `createSelector` from Reselect for all computed values\n- Create selector factories for parameterized selectors (per-track state)\n- Keep selectors pure and referentially stable\n\n### 8.2 Component Optimization\n\n- Wrap track components in `React.memo`\n- Use track-specific selectors to prevent unnecessary rerenders\n- Only subscribe to state slices that component needs\n\n### 8.3 Time Update Throttling\n\n- Throttle time updates to max 1 per second\n- Consider using requestAnimationFrame for progress bar updates\n- Batch multiple state updates when possible\n\n### 8.4 Entity Normalization\n\n- Store tracks in normalized entity format: `{ byId: {}, allIds: [] }`\n- Prevents deep equality checks on large arrays\n- Enables efficient lookups and updates\n\n### 8.5 Avoid Rerendering Track Lists\n\n```typescript\n// Bad: This causes all tracks to rerender on any player state change\nconst tracks = useSelector((state) => state.player.queue)\n\n// Good: Memoized selector that only changes when queue IDs change\nconst trackIds = useSelector(selectQueueTrackIds, shallowEqual)\n```\n\n## 9. Additional Features & Requirements\n\n### 9.1 Persistence\n\n- Save volume preference to localStorage\n- Save repeat/shuffle mode preferences\n- Option to save queue state for session recovery\n- Remember last played track and position\n\n### 9.2 Keyboard Shortcuts\n\n- Space: Toggle play/pause\n- Arrow Left: Seek backward 5s\n- Arrow Right: Seek forward 5s\n- Arrow Up: Volume up\n- Arrow Down: Volume down\n- M: Toggle mute\n- N: Next track\n- P: Previous track\n\n### 9.3 Media Session API\n\n- Integrate with browser's media session API\n- Show notifications with track info\n- Enable hardware media key support\n- Display metadata in OS media controls\n\n### 9.4 Crossfade (Optional Enhancement)\n\n- Smooth transitions between tracks\n- Configurable crossfade duration\n- Use two Audio elements for seamless playback\n\n### 9.5 Gapless Playback\n\n- Preload next track in queue\n- Minimize pause between tracks\n- Handle different audio formats gracefully\n\n### 9.6 Analytics & Telemetry\n\n- Track play counts\n- Track skip behavior\n- Track completion rates\n- User preferences and patterns\n\n### 9.7 Error Recovery\n\n- Retry failed track loads with exponential backoff\n- Skip unplayable tracks automatically\n- Show user-friendly error messages\n- Fallback to next track on repeated errors\n\n### 9.8 Loading States\n\n- Show loading indicator for tracks\n- Display buffering state\n- Handle slow network conditions\n- Preload album art\n\n### 9.9 Queue Manipulation\n\n- Drag and drop to reorder queue\n- Clear queue option\n- Save queue as playlist\n- View queue history\n\n### 9.10 Audio Effects (Future)\n\n- Equalizer settings\n- Playback speed control\n- Audio normalization\n- Bass boost\n\n## 10. Testing Considerations\n\n### 10.1 Unit Tests\n\n- Test all reducer logic\n- Test selector memoization\n- Test playback mode transitions\n- Test queue manipulation\n\n### 10.2 Integration Tests\n\n- Test middleware audio sync\n- Test event listener handling\n- Test error scenarios\n- Test playlist loading\n\n### 10.3 Performance Tests\n\n- Measure rerender frequency with 1000+ tracks\n- Profile selector performance\n- Test memory usage with large queues\n- Measure time update throttling effectiveness\n\n## 11. Implementation Phases\n\n### Phase 1: Core Playback (MVP)\n\n- Basic play/pause/stop\n- Single track playback\n- Volume control\n- Progress tracking\n\n### Phase 2: Queue & Navigation\n\n- Next/previous track\n- Queue management\n- Playlist support\n- Track navigation\n\n### Phase 3: Playback Modes\n\n- Repeat modes (off/one/all)\n- Shuffle mode\n- Auto-play next track\n\n### Phase 4: Performance & Polish\n\n- Optimize selectors\n- Add persistence\n- Error handling\n- Loading states\n\n### Phase 5: Enhanced Features\n\n- Keyboard shortcuts\n- Media Session API\n- Gapless playback\n- Advanced queue manipulation\n\n## 12. File Structure\n\n```\nsrc/player/\n├── player.ts                 // Audio instance\n├── playerSlice.ts           // Redux slice with reducers\n├── playerMiddleware.ts      // Audio sync middleware\n├── playerSelectors.ts       // Memoized selectors\n├── playerHooks.ts          // Custom React hooks\n├── utils/\n│   ├── shuffle.ts          // Fisher-Yates shuffle\n│   ├── format-time.ts       // Time formatting\n│   └── throttle.ts         // Throttle utility\n├── components/\n│   ├── PlayerControls.tsx\n│   ├── ProgressBar.tsx\n│   ├── VolumeControl.tsx\n│   ├── TrackItem.tsx\n│   └── QueueView.tsx\n├── types/\n│   └── player.types.ts     // TypeScript interfaces\n└── __tests__/\n    ├── playerSlice.test.ts\n    ├── selectors.test.ts\n    └── middleware.test.ts\n```\n\n## 13. API Contract\n\n### Track Entity (from API)\n\n```typescript\ninterface Track {\n  id: string\n  title: string\n  artist: string\n  album?: string\n  duration: number\n  url: string\n  albumArt?: string\n  artistId?: string\n  albumId?: string\n}\n```\n\n### Playlist Entity (from API)\n\n```typescript\ninterface Playlist {\n  id: string\n  name: string\n  description?: string\n  trackIds: string[]\n  createdAt: string\n  updatedAt: string\n  coverImage?: string\n}\n```\n\n## 14. Success Metrics\n\n- Zero unnecessary rerenders of non-playing tracks\n- < 100ms response time for play/pause actions\n- < 50ms for progress bar updates\n- Support 1000+ tracks in UI without performance degradation\n- < 1s time to start playing track after selection\n- Smooth 60fps animations for progress bars\n\n---\n\nThis specification provides a complete blueprint for implementing a production-ready music player with Redux Toolkit. Focus on Phase 1-3 for core functionality, then iterate with performance optimizations and enhanced features.\n"
  },
  {
    "path": "apps/rtk-query/src/player/index.ts",
    "content": "// Audio instance\nexport { audio } from './player'\n\n// Redux slice\nexport {\n  addToQueue,\n  clearError,\n  clearQueue,\n  handleTrackEnded,\n  insertNext,\n  // Queue actions\n  loadPlaylist,\n  // Navigation actions\n  nextTrack,\n  pause,\n  playerSlice,\n  // Playback control actions\n  playTrack,\n  playTrackAtIndex,\n  previousTrack,\n  removeFromQueue,\n  resume,\n  // Progress actions\n  seek,\n  setDuration,\n  // Error actions\n  setError,\n  // Metadata actions\n  setLoadingState,\n  setPlaybackState,\n  // Mode actions\n  setRepeatMode,\n  // Volume actions\n  setVolume,\n  stop,\n  toggleMute,\n  togglePlayPause,\n  toggleShuffle,\n  updateBuffered,\n  updateTime,\n} from './playerSlice'\n\n// Middleware\nexport { playerMiddleware } from './playerMiddleware'\n\n// Selectors\nexport {\n  makeSelectIsCurrentTrack,\n  makeSelectIsTrackInQueue,\n  // Selector factories\n  makeSelectTrackPlaybackState,\n  makeSelectTrackProgress,\n  makeSelectTrackQueuePosition,\n  selectBuffered,\n  selectCurrentPlaylistId,\n  selectCurrentTime,\n  selectCurrentTrack,\n  selectCurrentTrackId,\n  selectDuration,\n  selectEffectiveVolume,\n  selectError,\n  selectFormattedTime,\n  selectHasError,\n  selectHasNextTrack,\n  selectHasPreviousTrack,\n  selectIsLoading,\n  selectIsLoadingTrack,\n  selectIsMuted,\n  selectIsPaused,\n  // Computed selectors\n  selectIsPlaying,\n  selectNextTrackId,\n  selectOriginalQueue,\n  selectPlaybackModeDescription,\n  selectPlaybackState,\n  // Basic selectors\n  selectPlayerState,\n  selectPreviousTrackId,\n  selectProgress,\n  selectQueue,\n  selectQueueIndex,\n  selectQueueLength,\n  selectQueuePosition,\n  selectQueueTrackIds,\n  selectQueueTracks,\n  selectRepeatMode,\n  selectShuffleMode,\n  selectVolume,\n  selectVolumePercentage,\n} from './playerSelectors'\n\n// Hooks\nexport {\n  useCurrentTrack,\n  useIsCurrentTrack,\n  usePlaybackModes,\n  usePlaybackProgress,\n  usePlaybackState,\n  usePlayer,\n  usePlayerControls,\n  usePlayerKeyboardControls,\n  usePlayerQueue,\n  useQueue,\n  useQueueControls,\n  useTrackNavigation,\n  useTrackPlaybackState,\n  useTrackPlayer,\n  useTrackProgress,\n  useTrackQueuePosition,\n  useVolumeControl,\n} from './playerHooks'\n\n// Types\nexport type {\n  FormattedTime,\n  PlaybackState,\n  PlayerState,\n  Playlist,\n  RepeatMode,\n  Track,\n  TrackPlaybackState,\n  TrackProgress,\n} from './types/player.types'\n\n// Utilities\nexport { formatTime, parseTime } from './utils/format-time.ts'\nexport { shuffle, shuffleWithCurrentItem } from './utils/shuffle'\nexport { debounce, throttle } from './utils/throttle'\n"
  },
  {
    "path": "apps/rtk-query/src/player/player.ts",
    "content": "export const audio = new Audio()\n"
  },
  {
    "path": "apps/rtk-query/src/player/playerHooks.ts",
    "content": "import { useCallback, useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport type { AppDispatch, RootState } from '@/app/store/store'\n\nimport {\n  makeSelectIsCurrentTrack,\n  makeSelectTrackPlaybackState,\n  makeSelectTrackProgress,\n  makeSelectTrackQueuePosition,\n  selectCurrentTime,\n  selectCurrentTrack,\n  selectCurrentTrackId,\n  selectDuration,\n  selectEffectiveVolume,\n  selectError,\n  selectFormattedTime,\n  selectHasNextTrack,\n  selectHasPreviousTrack,\n  selectIsLoading,\n  selectIsMuted,\n  selectIsPaused,\n  selectIsPlaying,\n  selectNextTrackId,\n  selectPlaybackModeDescription,\n  selectPlaybackState,\n  selectPreviousTrackId,\n  selectProgress,\n  selectQueue,\n  selectQueueIndex,\n  selectQueuePosition,\n  selectQueueTracks,\n  selectRepeatMode,\n  selectShuffleMode,\n  selectVolume,\n  selectVolumePercentage,\n} from './playerSelectors'\nimport {\n  addToQueue,\n  clearQueue,\n  insertNext,\n  loadPlaylist,\n  nextTrack,\n  pause,\n  playTrack,\n  playTrackAtIndex,\n  previousTrack,\n  removeFromQueue,\n  resume,\n  seek,\n  setRepeatMode,\n  setVolume,\n  stop,\n  toggleMute,\n  togglePlayPause,\n  toggleShuffle,\n} from './playerSlice'\nimport type { RepeatMode, Track, TrackPlaybackState, TrackProgress } from './types/player.types'\n\n// ========================================\n// Playback Control Hooks\n// ========================================\n\nexport const usePlayingTrackProgress = () => {\n  const currentTime = useSelector(selectCurrentTime)\n  const duration = useSelector(selectDuration)\n  const playingTrackProgress = duration > 0 ? (currentTime / duration) * 100 : 0\n  return { playingTrackProgress }\n}\n\n/**\n * Hook for controlling playback (play, pause, stop, etc.)\n */\nexport function usePlayerControls() {\n  const dispatch = useDispatch<AppDispatch>()\n\n  return useMemo(\n    () => ({\n      play: (track: Track, playlistId?: string, tracks?: Track[]) =>\n        dispatch(playTrack({ track, playlistId, tracks })),\n      pause: () => dispatch(pause()),\n      resume: () => dispatch(resume()),\n      stop: () => dispatch(stop()),\n      togglePlayPause: () => dispatch(togglePlayPause()),\n      next: () => dispatch(nextTrack()),\n      previous: () => dispatch(previousTrack()),\n      playAtIndex: (index: number) => dispatch(playTrackAtIndex(index)),\n      seek: (time: number) => dispatch(seek(time)),\n    }),\n    [dispatch]\n  )\n}\n\n/**\n * Hook for playback state information\n */\nexport function usePlaybackState() {\n  const isPlaying = useSelector(selectIsPlaying)\n  const isPaused = useSelector(selectIsPaused)\n  const isLoading = useSelector(selectIsLoading)\n  const playbackState = useSelector(selectPlaybackState)\n  const error = useSelector(selectError)\n\n  return {\n    isPlaying,\n    isPaused,\n    isLoading,\n    playbackState,\n    error,\n  }\n}\n\n/**\n * Hook for current track information\n */\nexport function useCurrentTrack() {\n  const trackId = useSelector(selectCurrentTrackId)\n  const track = useSelector(selectCurrentTrack)\n  const isPlaying = useSelector(selectIsPlaying)\n  const isPaused = useSelector(selectIsPaused)\n\n  return {\n    trackId,\n    track,\n    isPlaying,\n    isPaused,\n  }\n}\n\n/**\n * Hook for playback progress\n */\nexport function usePlaybackProgress() {\n  const currentTime = useSelector(selectCurrentTime)\n  const duration = useSelector(selectDuration)\n  const progress = useSelector(selectProgress)\n  const formattedTime = useSelector(selectFormattedTime)\n\n  return {\n    currentTime,\n    duration,\n    progress,\n    formattedTime,\n  }\n}\n\n// ========================================\n// Track-Specific Hooks (Performance Optimized)\n// ========================================\n\n/**\n * Hook for track-specific playback state\n * Only causes rerender when THIS track's state changes\n *\n * Usage in track component:\n * ```tsx\n * const { isPlaying, isPaused } = useTrackPlaybackState(track.id);\n * ```\n */\nexport function useTrackPlaybackState(trackId: string): TrackPlaybackState {\n  const selectTrackState = useMemo(makeSelectTrackPlaybackState, [])\n  return useSelector((state: RootState) => selectTrackState(state, trackId))\n}\n\n/**\n * Hook for track-specific progress\n * Only returns progress data if this is the current track\n *\n * Usage in track component:\n * ```tsx\n * const { progress, currentTime } = useTrackProgress(track.id);\n * ```\n */\nexport function useTrackProgress(trackId: string): TrackProgress {\n  const selectProgress = useMemo(makeSelectTrackProgress, [])\n  return useSelector((state: RootState) => selectProgress(state, trackId))\n}\n\n/**\n * Hook to check if a track is the current track\n */\nexport function useIsCurrentTrack(trackId: string): boolean {\n  const selectIsCurrentTrack = useMemo(makeSelectIsCurrentTrack, [])\n  return useSelector((state: RootState) => selectIsCurrentTrack(state, trackId))\n}\n\n/**\n * Hook to get track's position in queue (null if not in queue)\n */\nexport function useTrackQueuePosition(trackId: string): number | null {\n  const selectPosition = useMemo(makeSelectTrackQueuePosition, [])\n  return useSelector((state: RootState) => selectPosition(state, trackId))\n}\n\n/**\n * Combined hook for track playback controls\n * Provides both state and control functions for a specific track\n */\nexport function useTrackPlayer(track: Track) {\n  const dispatch = useDispatch<AppDispatch>()\n  const { isPlaying, isPaused, isCurrentTrack } = useTrackPlaybackState(track.id)\n  const { progress, currentTime } = useTrackProgress(track.id)\n  const queuePosition = useTrackQueuePosition(track.id)\n\n  const play = useCallback(() => {\n    dispatch(playTrack({ track }))\n  }, [dispatch, track])\n\n  const pauseTrack = useCallback(() => {\n    dispatch(pause())\n  }, [dispatch])\n\n  const resumeTrack = useCallback(() => {\n    dispatch(resume())\n  }, [dispatch])\n\n  const togglePlayPauseTrack = useCallback(() => {\n    if (isPlaying) {\n      dispatch(pause())\n    } else if (isPaused) {\n      dispatch(resume())\n    } else {\n      dispatch(playTrack({ track }))\n    }\n  }, [dispatch, track, isPlaying, isPaused])\n\n  return {\n    // State\n    isPlaying,\n    isPaused,\n    isCurrentTrack,\n    progress,\n    currentTime,\n    queuePosition,\n    // Actions\n    play,\n    pause: pauseTrack,\n    resume: resumeTrack,\n    togglePlayPause: togglePlayPauseTrack,\n  }\n}\n\n// ========================================\n// Volume Control Hooks\n// ========================================\n\n/**\n * Hook for volume control\n */\nexport function useVolumeControl() {\n  const dispatch = useDispatch<AppDispatch>()\n  const volume = useSelector(selectVolume)\n  const isMuted = useSelector(selectIsMuted)\n  const effectiveVolume = useSelector(selectEffectiveVolume)\n  const volumePercentage = useSelector(selectVolumePercentage)\n\n  const setVolumeValue = useCallback(\n    (value: number) => {\n      dispatch(setVolume(value))\n    },\n    [dispatch]\n  )\n\n  const toggleMuteValue = useCallback(() => {\n    dispatch(toggleMute())\n  }, [dispatch])\n\n  return {\n    volume,\n    isMuted,\n    effectiveVolume,\n    volumePercentage,\n    setVolume: setVolumeValue,\n    toggleMute: toggleMuteValue,\n  }\n}\n\n// ========================================\n// Queue Management Hooks\n// ========================================\n\n/**\n * Hook for queue state\n */\nexport function useQueue() {\n  const queue = useSelector(selectQueue)\n  const queueIndex = useSelector(selectQueueIndex)\n  const queueTracks = useSelector(selectQueueTracks)\n  const queuePosition = useSelector(selectQueuePosition)\n  const hasNext = useSelector(selectHasNextTrack)\n  const hasPrevious = useSelector(selectHasPreviousTrack)\n  const nextTrackId = useSelector(selectNextTrackId)\n  const previousTrackId = useSelector(selectPreviousTrackId)\n\n  return {\n    queue,\n    queueIndex,\n    queueTracks,\n    queuePosition,\n    hasNext,\n    hasPrevious,\n    nextTrackId,\n    previousTrackId,\n  }\n}\n\n/**\n * Hook for queue manipulation\n */\nexport function useQueueControls() {\n  const dispatch = useDispatch<AppDispatch>()\n\n  return useMemo(\n    () => ({\n      loadPlaylist: (playlistId: string, tracks: Track[], startIndex?: number) =>\n        dispatch(loadPlaylist({ playlistId, tracks, startIndex })),\n      addToQueue: (tracks: Track[]) => dispatch(addToQueue(tracks)),\n      insertNext: (track: Track) => dispatch(insertNext(track)),\n      removeFromQueue: (index: number) => dispatch(removeFromQueue(index)),\n      clearQueue: () => dispatch(clearQueue()),\n    }),\n    [dispatch]\n  )\n}\n\n/**\n * Combined hook for queue state and controls\n */\nexport function usePlayerQueue() {\n  const queueState = useQueue()\n  const queueControls = useQueueControls()\n\n  return {\n    ...queueState,\n    ...queueControls,\n  }\n}\n\n// ========================================\n// Playback Mode Hooks\n// ========================================\n\n/**\n * Hook for playback modes (repeat, shuffle)\n */\nexport function usePlaybackModes() {\n  const dispatch = useDispatch<AppDispatch>()\n  const repeatMode = useSelector(selectRepeatMode)\n  const shuffleMode = useSelector(selectShuffleMode)\n  const modeDescription = useSelector(selectPlaybackModeDescription)\n\n  const setRepeatModeValue = useCallback(() => {\n    if (repeatMode === 'off') {\n      dispatch(setRepeatMode('one'))\n    }\n    if (repeatMode === 'one') {\n      dispatch(setRepeatMode('all'))\n    }\n    if (repeatMode === 'all') {\n      dispatch(setRepeatMode('off'))\n    }\n  }, [dispatch, repeatMode])\n\n  const toggleShuffleValue = useCallback(() => {\n    dispatch(toggleShuffle())\n  }, [dispatch])\n\n  const cycleRepeatMode = useCallback(() => {\n    const nextMode: RepeatMode = repeatMode === 'off' ? 'all' : repeatMode === 'all' ? 'one' : 'off'\n    dispatch(setRepeatMode(nextMode))\n  }, [dispatch, repeatMode])\n\n  return {\n    repeatMode,\n    shuffleMode,\n    modeDescription,\n    setRepeatMode: setRepeatModeValue,\n    toggleShuffle: toggleShuffleValue,\n    cycleRepeatMode,\n  }\n}\n\n// ========================================\n// Combined Player Hook\n// ========================================\n\n/**\n * All-in-one hook for complete player functionality\n * Use this if you need access to everything\n */\nexport function usePlayer() {\n  const controls = usePlayerControls()\n  const playbackState = usePlaybackState()\n  const currentTrack = useCurrentTrack()\n  const progress = usePlaybackProgress()\n  const volume = useVolumeControl()\n  const queue = usePlayerQueue()\n  const modes = usePlaybackModes()\n\n  return {\n    controls,\n    playbackState,\n    currentTrack,\n    progress,\n    volume,\n    queue,\n    modes,\n  }\n}\n\n// ========================================\n// Navigation Hooks\n// ========================================\n\n/**\n * Hook for track navigation (next, previous)\n */\nexport function useTrackNavigation() {\n  const dispatch = useDispatch<AppDispatch>()\n  const hasNext = useSelector(selectHasNextTrack)\n  const hasPrevious = useSelector(selectHasPreviousTrack)\n  const nextTrackId = useSelector(selectNextTrackId)\n  const previousTrackId = useSelector(selectPreviousTrackId)\n\n  const goNext = useCallback(() => {\n    dispatch(nextTrack())\n  }, [dispatch])\n\n  const goPrevious = useCallback(() => {\n    dispatch(previousTrack())\n  }, [dispatch])\n\n  return {\n    hasNext,\n    hasPrevious,\n    nextTrackId,\n    previousTrackId,\n    next: goNext,\n    previous: goPrevious,\n  }\n}\n\n// ========================================\n// Keyboard Controls Hook\n// ========================================\n\n/**\n * Hook to enable keyboard shortcuts for player control\n * Call this in your main player component\n *\n * Shortcuts:\n * - Space: Play/Pause\n * - Arrow Right: Seek forward 5s\n * - Arrow Left: Seek backward 5s\n * - Arrow Up: Volume up\n * - Arrow Down: Volume down\n * - M: Toggle mute\n * - N: Next track\n * - P: Previous track\n */\nexport function usePlayerKeyboardControls(enabled = true) {\n  const dispatch = useDispatch<AppDispatch>()\n  const currentTime = useSelector(selectCurrentTime)\n  const duration = useSelector(selectDuration)\n  const volume = useSelector(selectVolume)\n\n  const handleKeyPress = useCallback(\n    (e: KeyboardEvent) => {\n      if (!enabled) return\n\n      // Don't trigger if user is typing in an input\n      const target = e.target as HTMLElement\n      if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {\n        return\n      }\n\n      switch (e.key.toLowerCase()) {\n        case ' ':\n          e.preventDefault()\n          dispatch(togglePlayPause())\n          break\n\n        case 'arrowright':\n          e.preventDefault()\n          dispatch(seek(Math.min(currentTime + 5, duration)))\n          break\n\n        case 'arrowleft':\n          e.preventDefault()\n          dispatch(seek(Math.max(currentTime - 5, 0)))\n          break\n\n        case 'arrowup':\n          e.preventDefault()\n          dispatch(setVolume(Math.min(volume + 0.1, 1)))\n          break\n\n        case 'arrowdown':\n          e.preventDefault()\n          dispatch(setVolume(Math.max(volume - 0.1, 0)))\n          break\n\n        case 'm':\n          e.preventDefault()\n          dispatch(toggleMute())\n          break\n\n        case 'n':\n          e.preventDefault()\n          dispatch(nextTrack())\n          break\n\n        case 'p':\n          e.preventDefault()\n          dispatch(previousTrack())\n          break\n      }\n    },\n    [enabled, dispatch, currentTime, duration, volume]\n  )\n\n  // Set up keyboard event listener\n  useMemo(() => {\n    if (!enabled) return\n\n    if (typeof window !== 'undefined') {\n      window.addEventListener('keydown', handleKeyPress)\n      return () => window.removeEventListener('keydown', handleKeyPress)\n    }\n  }, [enabled, handleKeyPress])\n}\n\n// ========================================\n// Export all hooks\n// ========================================\n\nexport default {\n  usePlayer,\n  usePlayerControls,\n  usePlaybackState,\n  useCurrentTrack,\n  usePlaybackProgress,\n  useTrackPlaybackState,\n  useTrackProgress,\n  useIsCurrentTrack,\n  useTrackQueuePosition,\n  useTrackPlayer,\n  useVolumeControl,\n  useQueue,\n  useQueueControls,\n  usePlayerQueue,\n  usePlaybackModes,\n  useTrackNavigation,\n  usePlayerKeyboardControls,\n}\n"
  },
  {
    "path": "apps/rtk-query/src/player/playerMiddleware.ts",
    "content": "import { audio } from './player'\nimport {\n  handleTrackEnded,\n  nextTrack,\n  pause,\n  playTrack,\n  previousTrack,\n  resume,\n  seek,\n  setDuration,\n  setError,\n  setLoadingState,\n  setPlaybackState,\n  setVolume,\n  toggleMute,\n  updateBuffered,\n  updateTime,\n} from './playerSlice'\nimport { throttle } from './utils/throttle'\n\n// Flag to track if event listeners are already attached\nlet listenersAttached = false\n\n/**\n * Redux middleware that syncs player state with HTML5 Audio API\n * This middleware intercepts player actions and updates the audio element accordingly\n */\nexport const playerMiddleware = (store: any) => (next: any) => (action: any) => {\n  const result = next(action)\n  const state = store.getState()\n  const playerState = state.player\n\n  // Attach event listeners once\n  if (!listenersAttached) {\n    setupAudioEventListeners(store)\n    listenersAttached = true\n  }\n\n  // Handle different actions\n  if (playTrack.match(action)) {\n    const { track } = action.payload\n\n    // Get track URL from state\n    const trackUrl = track.url\n\n    if (trackUrl) {\n      audio.src = trackUrl\n      audio.currentTime = 0\n      audio\n        .play()\n        .then(() => {\n          store.dispatch(setPlaybackState('playing'))\n        })\n        .catch((error: Error) => {\n          store.dispatch(setError(error.message || 'Failed to play track'))\n        })\n    } else {\n      store.dispatch(setError('Track URL not found'))\n    }\n  } else if (\n    nextTrack.match(action) ||\n    previousTrack.match(action) ||\n    handleTrackEnded.match(action)\n  ) {\n    // Handle track switching - get the new current track and play it\n    const newState = store.getState()\n    const newCurrentTrackId = newState.player.currentTrackId\n    const newTrack = newCurrentTrackId ? newState.player.tracks[newCurrentTrackId] : null\n\n    if (newTrack && newTrack.url) {\n      audio.src = newTrack.url\n      audio.currentTime = 0\n      audio\n        .play()\n        .then(() => {\n          store.dispatch(setPlaybackState('playing'))\n        })\n        .catch((error: Error) => {\n          store.dispatch(setError(error.message || 'Failed to play next track'))\n        })\n    } else {\n      store.dispatch(setError('Next track URL not found'))\n    }\n  } else if (pause.match(action)) {\n    audio.pause()\n  } else if (resume.match(action)) {\n    audio\n      .play()\n      .then(() => {\n        store.dispatch(setPlaybackState('playing'))\n      })\n      .catch((error: Error) => {\n        store.dispatch(setError(error.message || 'Failed to resume playback'))\n      })\n  } else if (seek.match(action)) {\n    audio.currentTime = action.payload\n  } else if (setVolume.match(action)) {\n    audio.volume = action.payload\n  } else if (toggleMute.match(action)) {\n    audio.muted = playerState.isMuted\n  }\n\n  return result\n}\n\n/**\n * Sets up event listeners on the audio element\n * These listeners dispatch Redux actions when audio events occur\n */\nfunction setupAudioEventListeners(store: any) {\n  const dispatch = store.dispatch\n\n  // Throttled time update (max once per second)\n  const throttledTimeUpdate = throttle(() => {\n    dispatch(updateTime(audio.currentTime))\n  }, 1000)\n\n  // Track metadata loaded\n  audio.addEventListener('loadedmetadata', () => {\n    dispatch(setDuration(audio.duration))\n    dispatch(setLoadingState(false))\n  })\n\n  // Track time updates\n  audio.addEventListener('timeupdate', throttledTimeUpdate)\n\n  // Track ended\n  audio.addEventListener('ended', () => {\n    dispatch(handleTrackEnded())\n  })\n\n  // Error occurred\n  audio.addEventListener('error', () => {\n    const errorMessage =\n      audio.error?.message || `Error code: ${audio.error?.code}` || 'Unknown playback error'\n    dispatch(setError(errorMessage))\n  })\n\n  // Loading started\n  audio.addEventListener('loadstart', () => {\n    dispatch(setLoadingState(true))\n  })\n\n  // Can start playing\n  audio.addEventListener('canplay', () => {\n    dispatch(setLoadingState(false))\n  })\n\n  // Buffering progress\n  audio.addEventListener('progress', () => {\n    if (audio.buffered.length > 0 && audio.duration > 0) {\n      const buffered = (audio.buffered.end(audio.buffered.length - 1) / audio.duration) * 100\n      dispatch(updateBuffered(buffered))\n    }\n  })\n\n  // Waiting for data (buffering)\n  audio.addEventListener('waiting', () => {\n    dispatch(setLoadingState(true))\n  })\n\n  // Playing after waiting\n  audio.addEventListener('playing', () => {\n    dispatch(setLoadingState(false))\n    dispatch(setPlaybackState('playing'))\n  })\n\n  // Paused\n  audio.addEventListener('pause', () => {\n    // Only update state if not transitioning to a new track\n    if (audio.currentTime > 0) {\n      dispatch(setPlaybackState('paused'))\n    }\n  })\n\n  // Volume changed (from browser controls)\n  audio.addEventListener('volumechange', () => {\n    // Only dispatch if volume was changed externally (not from our actions)\n    // This prevents infinite loops\n    if (Math.abs(audio.volume - store.getState().player.volume) > 0.01) {\n      dispatch(setVolume(audio.volume))\n    }\n  })\n}\n"
  },
  {
    "path": "apps/rtk-query/src/player/playerSelectors.ts",
    "content": "import { createSelector } from '@reduxjs/toolkit'\n\nimport type { RootState } from '@/app/store/store'\n\nimport type { FormattedTime, TrackPlaybackState, TrackProgress } from './types/player.types'\nimport { formatTime } from './utils/format-time.ts'\n\n// ========================================\n// Basic Selectors\n// ========================================\n\nexport const selectPlayerState = (state: RootState) => state.player\n\nexport const selectCurrentTrackId = (state: RootState) => state.player.currentTrackId\n\nexport const selectCurrentPlaylistId = (state: RootState) => state.player.currentPlaylistId\n\nexport const selectPlaybackState = (state: RootState) => state.player.playbackState\n\nexport const selectCurrentTime = (state: RootState) => state.player.currentTime\n\nexport const selectDuration = (state: RootState) => state.player.duration\n\nexport const selectBuffered = (state: RootState) => state.player.buffered\n\nexport const selectVolume = (state: RootState) => state.player.volume\n\nexport const selectIsMuted = (state: RootState) => state.player.isMuted\n\nexport const selectRepeatMode = (state: RootState) => state.player.repeatMode\n\nexport const selectShuffleMode = (state: RootState) => state.player.shuffleMode\n\nexport const selectQueue = (state: RootState) => state.player.queue\n\nexport const selectOriginalQueue = (state: RootState) => state.player.originalQueue\n\nexport const selectQueueIndex = (state: RootState) => state.player.queueIndex\n\nexport const selectError = (state: RootState) => state.player.error\n\nexport const selectIsLoadingTrack = (state: RootState) => state.player.isLoadingTrack\n\nexport const selectHasNextTrack = (state: RootState) => state.player.hasNextTrack\n\nexport const selectHasPreviousTrack = (state: RootState) => state.player.hasPreviousTrack\n\n// ========================================\n// Computed Selectors (Memoized)\n// ========================================\n\n/**\n * Returns true if currently playing\n */\nexport const selectIsPlaying = createSelector(\n  [selectPlaybackState],\n  (playbackState) => playbackState === 'playing'\n)\n\n/**\n * Returns true if currently paused\n */\nexport const selectIsPaused = createSelector(\n  [selectPlaybackState],\n  (playbackState) => playbackState === 'paused'\n)\n\n/**\n * Returns true if in loading state\n */\nexport const selectIsLoading = createSelector(\n  [selectPlaybackState, selectIsLoadingTrack],\n  (playbackState, isLoadingTrack) => playbackState === 'loading' || isLoadingTrack\n)\n\n/**\n * Returns true if in error state\n */\nexport const selectHasError = createSelector(\n  [selectPlaybackState, selectError],\n  (playbackState, error) => playbackState === 'error' || error !== null\n)\n\n/**\n * Returns playback progress as percentage (0-100)\n */\nexport const selectProgress = createSelector(\n  [selectCurrentTime, selectDuration],\n  (currentTime, duration) => {\n    if (!duration || duration === 0) return 0\n    return (currentTime / duration) * 100\n  }\n)\n\n/**\n * Returns formatted time strings\n */\nexport const selectFormattedTime = createSelector(\n  [selectCurrentTime, selectDuration],\n  (currentTime, duration): FormattedTime => ({\n    current: formatTime(currentTime),\n    duration: formatTime(duration),\n  })\n)\n\n/**\n * Returns the current track from player state\n */\nexport const selectCurrentTrack = createSelector(\n  [selectCurrentTrackId, (state: RootState) => state.player.tracks],\n  (trackId, tracks) => {\n    if (!trackId) return null\n    return tracks[trackId] || null\n  }\n)\n\n/**\n * Returns array of track objects from the queue\n */\nexport const selectQueueTracks = createSelector(\n  [selectQueue, (state: RootState) => state.player.tracks],\n  (queue, tracks) => {\n    if (!queue.length) return []\n\n    return queue.map((trackId) => tracks[trackId]).filter((track) => track !== undefined)\n  }\n)\n\n/**\n * Returns just the track IDs from the queue (for list rendering)\n */\nexport const selectQueueTrackIds = createSelector([selectQueue], (queue) => queue)\n\n/**\n * Returns the number of tracks in queue\n */\nexport const selectQueueLength = createSelector([selectQueue], (queue) => queue.length)\n\n/**\n * Returns whether a track is in the queue\n */\nexport const makeSelectIsTrackInQueue = () =>\n  createSelector([selectQueue, (state: RootState, trackId: string) => trackId], (queue, trackId) =>\n    queue.includes(trackId)\n  )\n\n// ========================================\n// Track-Specific Selectors (Performance Critical)\n// ========================================\n\n/**\n * Factory function that creates a memoized selector for a specific track's playback state\n * This ensures only the specific track component rerenders when its state changes\n *\n * Usage:\n * ```\n * const selectTrackState = useMemo(makeSelectTrackPlaybackState, []);\n * const trackState = useSelector(state => selectTrackState(state, trackId));\n * ```\n */\nexport const makeSelectTrackPlaybackState = () =>\n  createSelector(\n    [selectCurrentTrackId, selectPlaybackState, (state: RootState, trackId: string) => trackId],\n    (currentTrackId, playbackState, trackId): TrackPlaybackState => ({\n      isCurrentTrack: currentTrackId === trackId,\n      isPlaying: currentTrackId === trackId && playbackState === 'playing',\n      isPaused: currentTrackId === trackId && playbackState === 'paused',\n      playbackState: currentTrackId === trackId ? playbackState : 'idle',\n    })\n  )\n\n/**\n * Factory function that creates a memoized selector for a specific track's progress\n * Only returns progress data if this is the current track\n *\n * Usage:\n * ```\n * const selectProgress = useMemo(makeSelectTrackProgress, []);\n * const progress = useSelector(state => selectProgress(state, trackId));\n * ```\n */\nexport const makeSelectTrackProgress = () =>\n  createSelector(\n    [\n      selectCurrentTrackId,\n      selectCurrentTime,\n      selectDuration,\n      (state: RootState, trackId: string) => trackId,\n    ],\n    (currentTrackId, currentTime, duration, trackId): TrackProgress => {\n      if (currentTrackId !== trackId) {\n        return { progress: 0, currentTime: 0 }\n      }\n\n      return {\n        progress: duration > 0 ? (currentTime / duration) * 100 : 0,\n        currentTime,\n      }\n    }\n  )\n\n/**\n * Returns whether a specific track is the current track\n */\nexport const makeSelectIsCurrentTrack = () =>\n  createSelector(\n    [selectCurrentTrackId, (state: RootState, trackId: string) => trackId],\n    (currentTrackId, trackId) => currentTrackId === trackId\n  )\n\n/**\n * Returns the position of a track in the queue\n */\nexport const makeSelectTrackQueuePosition = () =>\n  createSelector(\n    [selectQueue, (state: RootState, trackId: string) => trackId],\n    (queue, trackId) => {\n      const index = queue.indexOf(trackId)\n      return index === -1 ? null : index\n    }\n  )\n\n// ========================================\n// Volume & Controls Selectors\n// ========================================\n\n/**\n * Returns effective volume (0 if muted, otherwise actual volume)\n */\nexport const selectEffectiveVolume = createSelector(\n  [selectVolume, selectIsMuted],\n  (volume, isMuted) => (isMuted ? 0 : volume)\n)\n\n/**\n * Returns volume as percentage (0-100)\n */\nexport const selectVolumePercentage = createSelector([selectVolume], (volume) =>\n  Math.round(volume * 100)\n)\n\n// ========================================\n// Queue Information Selectors\n// ========================================\n\n/**\n * Returns information about the current position in queue\n */\nexport const selectQueuePosition = createSelector(\n  [selectQueueIndex, selectQueueLength],\n  (index, length) => ({\n    current: index + 1,\n    total: length,\n    isFirst: index === 0,\n    isLast: index === length - 1,\n  })\n)\n\n/**\n * Returns the next track ID in queue (if any)\n */\nexport const selectNextTrackId = createSelector(\n  [selectQueue, selectQueueIndex, selectRepeatMode],\n  (queue, queueIndex, repeatMode) => {\n    if (queue.length === 0) return null\n\n    const isAtEnd = queueIndex >= queue.length - 1\n\n    if (isAtEnd) {\n      if (repeatMode === 'one') {\n        return queue[queueIndex]\n      } else if (repeatMode === 'all') {\n        return queue[0]\n      } else {\n        return null\n      }\n    }\n\n    return queue[queueIndex + 1]\n  }\n)\n\n/**\n * Returns the previous track ID in queue (if any)\n */\nexport const selectPreviousTrackId = createSelector(\n  [selectQueue, selectQueueIndex, selectRepeatMode],\n  (queue, queueIndex, repeatMode) => {\n    if (queue.length === 0) return null\n\n    const isAtBeginning = queueIndex <= 0\n\n    if (isAtBeginning) {\n      if (repeatMode === 'all') {\n        return queue[queue.length - 1]\n      } else {\n        return queue[0] // Current track (restart)\n      }\n    }\n\n    return queue[queueIndex - 1]\n  }\n)\n\n// ========================================\n// Playback Mode Indicators\n// ========================================\n\n/**\n * Returns a user-friendly description of current playback mode\n */\nexport const selectPlaybackModeDescription = createSelector(\n  [selectRepeatMode, selectShuffleMode],\n  (repeatMode, shuffleMode) => {\n    const parts: string[] = []\n\n    if (shuffleMode) parts.push('Shuffle')\n\n    switch (repeatMode) {\n      case 'one':\n        parts.push('Repeat One')\n        break\n      case 'all':\n        parts.push('Repeat All')\n        break\n      case 'off':\n        parts.push('No Repeat')\n        break\n    }\n\n    return parts.join(', ')\n  }\n)\n"
  },
  {
    "path": "apps/rtk-query/src/player/playerSlice.ts",
    "content": "import type { PayloadAction } from '@reduxjs/toolkit'\nimport { createSlice } from '@reduxjs/toolkit'\n\nimport type { PlayerState, RepeatMode, Track } from './types/player.types'\nimport { shuffle, shuffleWithCurrentItem } from './utils/shuffle'\n\n// Load persisted preferences from localStorage\nconst loadPersistedVolume = (): number => {\n  try {\n    const volume = localStorage.getItem('player_volume')\n    return volume ? parseFloat(volume) : 1\n  } catch {\n    return 1\n  }\n}\n\nconst loadPersistedRepeatMode = (): RepeatMode => {\n  try {\n    const mode = localStorage.getItem('player_repeat_mode')\n    return (mode as RepeatMode) || 'one'\n  } catch {\n    return 'off'\n  }\n}\n\nconst loadPersistedShuffle = (): boolean => {\n  try {\n    const shuffle = localStorage.getItem('player_shuffle')\n    return shuffle === 'true'\n  } catch {\n    return false\n  }\n}\n\nconst initialState: PlayerState = {\n  // Current playback state\n  currentTrackId: null,\n  currentPlaylistId: null,\n  playbackState: 'idle',\n\n  // Playback position\n  currentTime: 0,\n  duration: 0,\n  buffered: 0,\n\n  // Volume control\n  volume: loadPersistedVolume(),\n  isMuted: false,\n\n  // Playback modes\n  repeatMode: loadPersistedRepeatMode(),\n  shuffleMode: loadPersistedShuffle(),\n\n  // Queue management\n  queue: [],\n  originalQueue: [],\n  queueIndex: -1,\n\n  // Track entities\n  tracks: {},\n\n  // Error handling\n  error: null,\n\n  // Additional metadata\n  isLoadingTrack: false,\n  hasNextTrack: false,\n  hasPreviousTrack: false,\n}\n\nexport const playerSlice = createSlice({\n  name: 'player',\n  initialState,\n  reducers: {\n    // ========================================\n    // Playback Control\n    // ========================================\n\n    playTrack: (\n      state,\n      action: PayloadAction<{\n        track: Track\n        playlistId?: string\n        tracks?: Track[]\n      }>\n    ) => {\n      const { track, playlistId, tracks } = action.payload\n\n      // Store track in entities\n      state.tracks[track.id] = track\n\n      // If this is a new track\n      if (state.currentTrackId !== track.id) {\n        state.currentTrackId = track.id\n        state.currentPlaylistId = playlistId || null\n        state.currentTime = 0\n        state.duration = 0\n        state.buffered = 0\n        state.playbackState = 'loading'\n        state.error = null\n\n        // If tracks array is provided, set up queue with all tracks\n        if (tracks && tracks.length > 0) {\n          // Store all tracks in entities\n          tracks.forEach((t) => {\n            state.tracks[t.id] = t\n          })\n\n          const trackIds = tracks.map((t) => t.id)\n          state.originalQueue = trackIds\n          state.queue = state.shuffleMode ? shuffle(trackIds) : trackIds\n          state.queueIndex = state.queue.indexOf(track.id)\n        } else if (state.queue.length === 0) {\n          // If no queue exists, create one with just this track\n          state.queue = [track.id]\n          state.originalQueue = [track.id]\n          state.queueIndex = 0\n        } else {\n          // Find track in existing queue\n          const index = state.queue.indexOf(track.id)\n          if (index !== -1) {\n            state.queueIndex = index\n          } else {\n            // Track not in queue, add it\n            state.queue.push(track.id)\n            state.originalQueue.push(track.id)\n            state.queueIndex = state.queue.length - 1\n          }\n        }\n      } else if (state.playbackState === 'paused') {\n        // Same track, just resume\n        state.playbackState = 'playing'\n      }\n\n      playerSlice.caseReducers.updateQueueMetadata(state)\n    },\n\n    pause: (state) => {\n      if (state.playbackState === 'playing') {\n        state.playbackState = 'paused'\n      }\n    },\n\n    resume: (state) => {\n      if (state.playbackState === 'paused') {\n        state.playbackState = 'playing'\n      }\n    },\n\n    stop: (state) => {\n      state.playbackState = 'idle'\n      state.currentTime = 0\n    },\n\n    togglePlayPause: (state) => {\n      if (state.playbackState === 'playing') {\n        state.playbackState = 'paused'\n      } else if (state.playbackState === 'paused' || state.playbackState === 'idle') {\n        state.playbackState = 'playing'\n      }\n    },\n\n    // ========================================\n    // Navigation\n    // ========================================\n\n    nextTrack: (state) => {\n      if (state.queue.length === 0) return\n\n      // Check if at end of queue\n      const isAtEnd = state.queueIndex >= state.queue.length - 1\n\n      if (isAtEnd) {\n        if (state.repeatMode === 'all') {\n          // Loop to beginning\n          state.queueIndex = 0\n          state.currentTrackId = state.queue[0]\n          state.currentTime = 0\n          state.playbackState = 'loading'\n        } else {\n          // Stop playback\n          state.playbackState = 'idle'\n          state.currentTime = 0\n        }\n      } else {\n        // Go to next track\n        state.queueIndex++\n        state.currentTrackId = state.queue[state.queueIndex]\n        state.currentTime = 0\n        state.playbackState = 'loading'\n      }\n\n      playerSlice.caseReducers.updateQueueMetadata(state)\n    },\n\n    previousTrack: (state) => {\n      if (state.queue.length === 0) return\n\n      // If more than 3 seconds into track, restart current track\n      if (state.currentTime > 3) {\n        state.currentTime = 0\n        return\n      }\n\n      // Check if at beginning of queue\n      const isAtBeginning = state.queueIndex <= 0\n\n      if (isAtBeginning) {\n        if (state.repeatMode === 'all') {\n          // Loop to end\n          state.queueIndex = state.queue.length - 1\n          state.currentTrackId = state.queue[state.queueIndex]\n          state.currentTime = 0\n          state.playbackState = 'loading'\n        } else {\n          // Restart current track\n          state.currentTime = 0\n        }\n      } else {\n        // Go to previous track\n        state.queueIndex--\n        state.currentTrackId = state.queue[state.queueIndex]\n        state.currentTime = 0\n        state.playbackState = 'loading'\n      }\n\n      playerSlice.caseReducers.updateQueueMetadata(state)\n    },\n\n    playTrackAtIndex: (state, action: PayloadAction<number>) => {\n      const index = action.payload\n\n      if (index >= 0 && index < state.queue.length) {\n        state.queueIndex = index\n        state.currentTrackId = state.queue[index]\n        state.currentTime = 0\n        state.playbackState = 'loading'\n        playerSlice.caseReducers.updateQueueMetadata(state)\n      }\n    },\n\n    handleTrackEnded: (state) => {\n      // Repeat one - replay current track\n      if (state.repeatMode === 'one') {\n        state.currentTime = 0\n        return\n      }\n      // Automatically play next track when current track ends\n      playerSlice.caseReducers.nextTrack(state)\n    },\n\n    // ========================================\n    // Progress\n    // ========================================\n\n    seek: (state, action: PayloadAction<number>) => {\n      const time = action.payload\n\n      // Validate position\n      if (time >= 0 && time <= state.duration) {\n        state.currentTime = time\n      }\n    },\n\n    updateTime: (state, action: PayloadAction<number>) => {\n      state.currentTime = action.payload\n    },\n\n    updateBuffered: (state, action: PayloadAction<number>) => {\n      state.buffered = Math.max(0, Math.min(100, action.payload))\n    },\n\n    setDuration: (state, action: PayloadAction<number>) => {\n      state.duration = action.payload\n    },\n\n    // ========================================\n    // Volume\n    // ========================================\n\n    setVolume: (state, action: PayloadAction<number>) => {\n      const volume = Math.max(0, Math.min(1, action.payload))\n      state.volume = volume\n\n      // Auto-unmute if volume > 0\n      if (volume > 0) {\n        state.isMuted = false\n      }\n\n      // Persist to localStorage\n      try {\n        localStorage.setItem('player_volume', volume.toString())\n      } catch {\n        // Ignore localStorage errors\n      }\n    },\n\n    toggleMute: (state) => {\n      state.isMuted = !state.isMuted\n    },\n\n    // ========================================\n    // Playback Modes\n    // ========================================\n\n    setRepeatMode: (state, action: PayloadAction<RepeatMode>) => {\n      state.repeatMode = action.payload\n      playerSlice.caseReducers.updateQueueMetadata(state)\n\n      // Persist to localStorage\n      try {\n        localStorage.setItem('player_repeat_mode', action.payload)\n      } catch {\n        // Ignore localStorage errors\n      }\n    },\n\n    toggleShuffle: (state) => {\n      state.shuffleMode = !state.shuffleMode\n\n      if (state.shuffleMode) {\n        // Enable shuffle\n        if (state.currentTrackId) {\n          // Shuffle but keep current track at current position\n          state.queue = shuffleWithCurrentItem(\n            state.originalQueue,\n            state.originalQueue.indexOf(state.currentTrackId)\n          )\n          state.queueIndex = 0 // Current track is now at index 0\n        } else {\n          state.queue = shuffle(state.originalQueue)\n        }\n      } else {\n        // Disable shuffle - restore original order\n        state.queue = [...state.originalQueue]\n\n        // Find current track in original queue\n        if (state.currentTrackId) {\n          state.queueIndex = state.queue.indexOf(state.currentTrackId)\n        }\n      }\n\n      playerSlice.caseReducers.updateQueueMetadata(state)\n\n      // Persist to localStorage\n      try {\n        localStorage.setItem('player_shuffle', state.shuffleMode.toString())\n      } catch {\n        // Ignore localStorage errors\n      }\n    },\n\n    // ========================================\n    // Queue Management\n    // ========================================\n\n    loadPlaylist: (\n      state,\n      action: PayloadAction<{\n        playlistId: string\n        tracks: Track[]\n        startIndex?: number\n      }>\n    ) => {\n      const { playlistId, tracks, startIndex = 0 } = action.payload\n\n      // Store all tracks in entities\n      tracks.forEach((track) => {\n        state.tracks[track.id] = track\n      })\n\n      const trackIds = tracks.map((t) => t.id)\n      state.currentPlaylistId = playlistId\n      state.originalQueue = trackIds\n      state.queue = state.shuffleMode ? shuffle(trackIds) : trackIds\n      state.queueIndex = Math.max(0, Math.min(startIndex, tracks.length - 1))\n      state.currentTrackId = null\n      state.currentTime = 0\n      state.playbackState = 'loading'\n\n      playerSlice.caseReducers.updateQueueMetadata(state)\n    },\n\n    addToQueue: (state, action: PayloadAction<Track[]>) => {\n      const tracks = action.payload\n\n      // Store tracks in entities\n      tracks.forEach((track) => {\n        state.tracks[track.id] = track\n      })\n\n      const trackIds = tracks.map((t) => t.id)\n      state.originalQueue.push(...trackIds)\n\n      if (state.shuffleMode) {\n        // In shuffle mode, add tracks in random positions\n        const shuffledNew = shuffle(trackIds)\n        state.queue.push(...shuffledNew)\n      } else {\n        state.queue.push(...trackIds)\n      }\n\n      playerSlice.caseReducers.updateQueueMetadata(state)\n    },\n\n    insertNext: (state, action: PayloadAction<Track>) => {\n      const track = action.payload\n\n      // Store track in entities\n      state.tracks[track.id] = track\n\n      // Find position in original queue\n      const currentOriginalIndex = state.originalQueue.indexOf(state.currentTrackId || '')\n\n      // Insert after current track in both queues\n      state.originalQueue.splice(currentOriginalIndex + 1, 0, track.id)\n      state.queue.splice(state.queueIndex + 1, 0, track.id)\n\n      playerSlice.caseReducers.updateQueueMetadata(state)\n    },\n\n    removeFromQueue: (state, action: PayloadAction<number>) => {\n      const index = action.payload\n\n      if (index < 0 || index >= state.queue.length) return\n\n      const trackId = state.queue[index]\n\n      // Remove from queue\n      state.queue.splice(index, 1)\n\n      // Remove from original queue\n      const originalIndex = state.originalQueue.indexOf(trackId)\n      if (originalIndex !== -1) {\n        state.originalQueue.splice(originalIndex, 1)\n      }\n\n      // Adjust queue index\n      if (index < state.queueIndex) {\n        state.queueIndex--\n      } else if (index === state.queueIndex) {\n        // Removing current track - play next\n        if (state.queue.length > 0) {\n          if (state.queueIndex >= state.queue.length) {\n            state.queueIndex = state.queue.length - 1\n          }\n          state.currentTrackId = state.queue[state.queueIndex]\n          state.currentTime = 0\n          state.playbackState = 'loading'\n        } else {\n          // Queue is empty\n          state.currentTrackId = null\n          state.queueIndex = -1\n          state.playbackState = 'idle'\n        }\n      }\n\n      playerSlice.caseReducers.updateQueueMetadata(state)\n    },\n\n    clearQueue: (state) => {\n      state.queue = []\n      state.originalQueue = []\n      state.queueIndex = -1\n      state.currentTrackId = null\n      state.currentPlaylistId = null\n      state.playbackState = 'idle'\n      state.currentTime = 0\n      state.duration = 0\n      playerSlice.caseReducers.updateQueueMetadata(state)\n    },\n\n    // ========================================\n    // Error Handling\n    // ========================================\n\n    setError: (state, action: PayloadAction<string>) => {\n      state.error = action.payload\n      state.playbackState = 'error'\n    },\n\n    clearError: (state) => {\n      state.error = null\n      if (state.playbackState === 'error') {\n        state.playbackState = 'idle'\n      }\n    },\n\n    // ========================================\n    // Metadata\n    // ========================================\n\n    setLoadingState: (state, action: PayloadAction<boolean>) => {\n      state.isLoadingTrack = action.payload\n      if (action.payload && state.playbackState !== 'error') {\n        state.playbackState = 'loading'\n      }\n    },\n\n    setPlaybackState: (state, action: PayloadAction<PlayerState['playbackState']>) => {\n      state.playbackState = action.payload\n    },\n\n    // ========================================\n    // Internal Helpers\n    // ========================================\n\n    updateQueueMetadata: (state) => {\n      // Update hasNext and hasPrevious flags\n      if (state.queue.length === 0) {\n        state.hasNextTrack = false\n        state.hasPreviousTrack = false\n        return\n      }\n\n      const isAtEnd = state.queueIndex >= state.queue.length - 1\n      const isAtBeginning = state.queueIndex <= 0\n\n      // Has next if not at end, or if repeat mode is on\n      state.hasNextTrack = !isAtEnd || state.repeatMode === 'all' || state.repeatMode === 'one'\n\n      // Has previous if not at beginning, or if repeat mode is 'all', or if more than 3 seconds into track\n      state.hasPreviousTrack = !isAtBeginning || state.repeatMode === 'all' || state.currentTime > 3\n    },\n  },\n})\n\nexport const {\n  // Playback control\n  playTrack,\n  pause,\n  resume,\n  stop,\n  togglePlayPause,\n\n  // Navigation\n  nextTrack,\n  previousTrack,\n  playTrackAtIndex,\n  handleTrackEnded,\n\n  // Progress\n  seek,\n  updateTime,\n  updateBuffered,\n  setDuration,\n\n  // Volume\n  setVolume,\n  toggleMute,\n\n  // Modes\n  setRepeatMode,\n  toggleShuffle,\n\n  // Queue\n  loadPlaylist,\n  addToQueue,\n  insertNext,\n  removeFromQueue,\n  clearQueue,\n\n  // Error handling\n  setError,\n  clearError,\n\n  // Metadata\n  setLoadingState,\n  setPlaybackState,\n} = playerSlice.actions\n\nexport default playerSlice.reducer\n"
  },
  {
    "path": "apps/rtk-query/src/player/task.md",
    "content": "@src/player/ inside this folder now we have 2 files. I need redux slice wrapper around Audio instance\n\n- in one moment only single track can be player\n- we can pause track and continues playing from the same moment.\n- if we start play new track the track starts from begin.\n- we can have ability send to redux info about all playlist inside which playing track exists. Beacaude we next functionlity:\n  - when track fininsh next song from playlist should play\n  - or other rundom track from player should play\n  - or the same track should repeat\n    depends on current mode: repeat one, repeat all, repeat off, shuffle...\n\nsuggest solution and create full doc about this task. i forgot somethig important so add additional\nrequirements in this specification\nto make good functional player.\n\nconcentrate only on Business logic. But u can give me examles of compoennt and how to use this inside component.\n\nEach component that render track should be able to visualize track state: is playing, is paused, and progress bar if\ncomponent's track is playing or paused track\n\ncreate selectors, think about perfomance: on the page we will have hundreds of tracks, so we should rerender each track compoennt\nif theirs track is different of current paused/played track\n"
  },
  {
    "path": "apps/rtk-query/src/player/types/player.types.ts",
    "content": "export interface Track {\n  id: string\n  title: string\n  artist: string\n  album?: string\n  duration: number // in seconds\n  url: string\n  albumArt?: string\n  artistId?: string\n  albumId?: string\n}\n\nexport interface Playlist {\n  id: string\n  name: string\n  description?: string\n  trackIds: string[]\n  createdAt: string\n  updatedAt: string\n  coverImage?: string\n}\n\nexport type PlaybackState = 'idle' | 'playing' | 'paused' | 'loading' | 'error'\n\nexport type RepeatMode = 'off' | 'one' | 'all'\n\nexport interface PlayerState {\n  // Current playback state\n  currentTrackId: string | null\n  currentPlaylistId: string | null\n  playbackState: PlaybackState\n\n  // Playback position\n  currentTime: number // in seconds\n  duration: number // in seconds\n  buffered: number // percentage 0-100\n\n  // Volume control\n  volume: number // 0-1\n  isMuted: boolean\n\n  // Playback modes\n  repeatMode: RepeatMode\n  shuffleMode: boolean\n\n  // Queue management\n  queue: string[] // ordered track IDs\n  originalQueue: string[] // original order before shuffle\n  queueIndex: number\n\n  // Track entities - normalized storage\n  tracks: Record<string, Track> // tracks stored by ID\n\n  // Error handling\n  error: string | null\n\n  // Additional metadata\n  isLoadingTrack: boolean\n  hasNextTrack: boolean\n  hasPreviousTrack: boolean\n}\n\nexport interface TrackPlaybackState {\n  isCurrentTrack: boolean\n  isPlaying: boolean\n  isPaused: boolean\n  playbackState: PlaybackState\n}\n\nexport interface TrackProgress {\n  progress: number\n  currentTime: number\n}\n\nexport interface FormattedTime {\n  current: string\n  duration: string\n}\n"
  },
  {
    "path": "apps/rtk-query/src/player/utils/convert-api-track-to-player-track.ts",
    "content": "import { type BaseAttributes, type TrackDetails } from '@/features/tracks/api/tracksApi.types.ts'\nimport { ImageType } from '@/shared/types/commonApi.types.ts'\nimport { getImageByType } from '@/shared/utils'\n\nimport type { Track } from '../types/player.types.ts'\n\ntype AnyTrack = TrackDetails<BaseAttributes & { user?: { id: string; name: string } }>\n\n/**\n * Converts API track response to Player Track format\n */\nexport const convertApiTrackToPlayerTrack = <T extends AnyTrack>(apiTrack: T): Track => {\n  const audioUrl = apiTrack.attributes.attachments?.[0]?.url || ''\n  const image = apiTrack.attributes.images\n    ? getImageByType(apiTrack.attributes.images, ImageType.MEDIUM)\n    : undefined\n  const coverUrl = image?.url || ''\n\n  const artistName = apiTrack.attributes.user?.name || 'Unknown Artist'\n  const artistId = apiTrack.relationships?.artists?.data?.[0]?.id\n\n  return {\n    id: apiTrack.id,\n    title: apiTrack.attributes.title,\n    artist: artistName,\n    duration: 0,\n    url: audioUrl,\n    albumArt: coverUrl,\n    artistId: artistId,\n  }\n}\n\n/**\n * Converts array of API tracks to Player Track format\n */\nexport const convertApiTracksToPlayerTracks = <T extends AnyTrack>(apiTracks: T[]): Track[] => {\n  return apiTracks.map(convertApiTrackToPlayerTrack)\n}\n"
  },
  {
    "path": "apps/rtk-query/src/player/utils/format-time.ts",
    "content": "/**\n * Formats seconds into MM:SS or HH:MM:SS format\n */\nexport function formatTime(seconds: number): string {\n  if (!isFinite(seconds) || seconds < 0) {\n    return '0:00'\n  }\n\n  const hours = Math.floor(seconds / 3600)\n  const minutes = Math.floor((seconds % 3600) / 60)\n  const secs = Math.floor(seconds % 60)\n\n  if (hours > 0) {\n    return `${hours}:${padZero(minutes)}:${padZero(secs)}`\n  }\n\n  return `${minutes}:${padZero(secs)}`\n}\n\n/**\n * Pads a number with leading zero if less than 10\n */\nfunction padZero(num: number): string {\n  return num.toString().padStart(2, '0')\n}\n\n/**\n * Parses a time string (MM:SS or HH:MM:SS) into seconds\n */\nexport function parseTime(timeString: string): number {\n  const parts = timeString.split(':').map(Number)\n\n  if (parts.length === 2) {\n    // MM:SS\n    return parts[0] * 60 + parts[1]\n  } else if (parts.length === 3) {\n    // HH:MM:SS\n    return parts[0] * 3600 + parts[1] * 60 + parts[2]\n  }\n\n  return 0\n}\n"
  },
  {
    "path": "apps/rtk-query/src/player/utils/index.ts",
    "content": "export { formatTime, parseTime } from './format-time.ts'\nexport { shuffle, shuffleWithCurrentItem } from './shuffle'\nexport { debounce, throttle } from './throttle'\n"
  },
  {
    "path": "apps/rtk-query/src/player/utils/shuffle.ts",
    "content": "/**\n * Fisher-Yates shuffle algorithm\n * Shuffles an array in place and returns it\n */\nexport function shuffle<T>(array: T[]): T[] {\n  const shuffled = [...array]\n\n  for (let i = shuffled.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1))\n    ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]\n  }\n\n  return shuffled\n}\n\n/**\n * Shuffles an array but keeps the current item at its position\n * Useful when enabling shuffle mode while a track is playing\n */\nexport function shuffleWithCurrentItem<T>(array: T[], currentIndex: number): T[] {\n  if (currentIndex < 0 || currentIndex >= array.length) {\n    return shuffle(array)\n  }\n\n  const currentItem = array[currentIndex]\n  const otherItems = array.filter((_, index) => index !== currentIndex)\n  const shuffledOthers = shuffle(otherItems)\n\n  // Insert current item at the beginning\n  return [currentItem, ...shuffledOthers]\n}\n"
  },
  {
    "path": "apps/rtk-query/src/player/utils/throttle.ts",
    "content": "/**\n * Throttles a function to be called at most once per specified time period\n */\nexport function throttle<T extends (...args: any[]) => any>(\n  func: T,\n  limit: number\n): (...args: Parameters<T>) => void {\n  let inThrottle: boolean = false\n  let lastArgs: Parameters<T> | null = null\n\n  return function (this: any, ...args: Parameters<T>) {\n    if (!inThrottle) {\n      func.apply(this, args)\n      inThrottle = true\n      lastArgs = null\n\n      setTimeout(() => {\n        inThrottle = false\n        if (lastArgs) {\n          func.apply(this, lastArgs)\n          lastArgs = null\n        }\n      }, limit)\n    } else {\n      lastArgs = args\n    }\n  }\n}\n\n/**\n * Debounces a function to be called after a specified delay\n */\nexport function debounce<T extends (...args: any[]) => any>(\n  func: T,\n  delay: number\n): (...args: Parameters<T>) => void {\n  let timeoutId: NodeJS.Timeout | null = null\n\n  return function (this: any, ...args: Parameters<T>) {\n    if (timeoutId) {\n      clearTimeout(timeoutId)\n    }\n\n    timeoutId = setTimeout(() => {\n      func.apply(this, args)\n    }, delay)\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/AudioPlayer/AudioPlayer.module.css",
    "content": ".player {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  width: 100%;\n  min-height: 64px;\n\n  background: var(--color-bg-primary);\n}\n\n.trackInfo {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  min-width: 200px;\n}\n\n.cover {\n  width: 112px;\n  height: 112px;\n  border-radius: 4px;\n  background: var(--color-bg-card);\n}\n\n.cover img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.info {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.playerControls {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 8px;\n  align-items: center;\n}\n\n.controls {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n}\n\n.playPauseButton {\n  width: 48px;\n  height: 48px;\n}\n\n.active {\n  color: var(--color-accent);\n}\n\n.iconButton.active:hover,\n.iconButton.active:focus {\n  color: var(--color-accent);\n}\n\n.progressBar {\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  max-width: 632px;\n}\n\n.time {\n  min-width: 36px;\n  font-size: var(--font-size-xs);\n  color: var(--color-text-secondary);\n  text-align: center;\n}\n\n.progress {\n  cursor: pointer;\n\n  height: 5px;\n  border: none;\n  border-radius: 4px;\n\n  accent-color: var(--color-text-primary);\n}\n\n.trackProgress {\n  width: 100%;\n  max-width: 550px;\n}\n\n.volumeColumn {\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n\n  min-width: 160px;\n  padding-right: 32px;\n}\n\n.volumeProgress {\n  width: 119px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/AudioPlayer/AudioPlayer.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { type RepeatMode, usePlaybackState } from '@/player/index.ts'\n\nimport { AudioPlayer } from './AudioPlayer.tsx'\n\nconst meta = {\n  title: 'Components/Player',\n  component: AudioPlayer,\n  parameters: {},\n  args: {},\n} satisfies Meta<typeof AudioPlayer>\n\nexport default meta\n\nconst demoTrack = {\n  src: 'https://cdn.uppbeat.io/audio-files/c636d7c86452449b1203fc0bded83e29/4358717fc9da477a52fb18a6cbd3afcc/d154b5ce5ff1a05ae8115a3c678062e8/STREAMING-dreamland-matrika-main-version-31140-02-25.mp3',\n  cover: 'https://unsplash.it/112/112',\n  title: 'Play It Safe',\n  artist: 'Julia Wolf',\n}\n\nexport const Basic = {\n  render: () => {\n    const { isPlaying } = usePlaybackState()\n\n    const [isShuffle, setIsShuffle] = useState(false)\n    const [isRepeat, setIsRepeat] = useState<RepeatMode>('off')\n\n    const [track] = useState(demoTrack)\n    return (\n      <AudioPlayer\n        cover={track.cover}\n        title={track.title}\n        artist={track.artist}\n        isPlaying={isPlaying}\n        onNext={() => {}}\n        onPrevious={() => {}}\n        onTogglePlay={() => {}}\n        isShuffle={isShuffle}\n        isRepeat={isRepeat}\n        onShuffle={() => setIsShuffle(!isShuffle)}\n        onRepeat={() => setIsRepeat('one')}\n        duration={0}\n        currentTime={0}\n        volume={1}\n        onTimeSeek={() => {}}\n        onVolumeSet={() => {}}\n      />\n    )\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/AudioPlayer/AudioPlayer.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport type { RepeatMode } from '@/player'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport {\n  PauseIcon,\n  PlayIcon,\n  RepeatIcon,\n  ShuffleIcon,\n  SkipNextIcon,\n  SkipPreviousIcon,\n  VolumeIcon,\n  VolumeMuteIcon,\n} from '@/shared/icons'\nimport { IconOneRepeat } from '@/shared/icons/IconOneRepeat.tsx'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './AudioPlayer.module.css'\n\nexport type PlayerProps = {\n  cover: string\n  title: string\n  artist: string\n  isPlaying: boolean\n  onNext: () => void\n  onPrevious: () => void\n  onTogglePlay: () => void\n  isShuffle: boolean\n  isRepeat: RepeatMode\n  onShuffle: () => void\n  onRepeat: () => void\n  duration: number\n  currentTime: number\n  volume: number\n  onTimeSeek: (time: number) => void\n  onVolumeSet: (volume: number) => void\n} & ComponentProps<'div'>\n\nexport const AudioPlayer = ({\n  cover = noCoverPlaceholder,\n  title,\n  artist,\n  isPlaying,\n  onNext,\n  onPrevious,\n  onTogglePlay,\n  isShuffle,\n  isRepeat,\n  onShuffle,\n  onRepeat,\n  className,\n  duration,\n  currentTime,\n  volume,\n  onTimeSeek,\n  onVolumeSet,\n  ...props\n}: PlayerProps) => {\n  const handleChangeTime = (e: React.ChangeEvent<HTMLInputElement>) => {\n    onTimeSeek(Number(e.target.value))\n  }\n\n  const handleVolume = (e: React.ChangeEvent<HTMLInputElement>) => {\n    onVolumeSet(Number(e.target.value))\n  }\n\n  const handleVolumeMute = () => {\n    onVolumeSet(volume > 0 ? 0 : 1)\n  }\n\n  return (\n    <div className={clsx(s.player, className)} {...props}>\n      <div className={s.trackInfo}>\n        <div className={s.cover}>\n          <img src={cover} alt=\"cover\" />\n        </div>\n        <div className={s.info}>\n          <Typography variant=\"body1\" as=\"h3\">\n            {title}\n          </Typography>\n          <Typography variant=\"body2\" as=\"p\">\n            {artist}\n          </Typography>\n        </div>\n      </div>\n\n      <div className={s.playerControls}>\n        <div className={s.controls}>\n          <IconButton onClick={onShuffle} className={clsx(s.iconButton, isShuffle && s.active)}>\n            <ShuffleIcon />\n          </IconButton>\n          <IconButton onClick={onPrevious}>\n            <SkipPreviousIcon />\n          </IconButton>\n          <IconButton className={s.playPauseButton} onClick={onTogglePlay}>\n            {isPlaying ? <PauseIcon /> : <PlayIcon />}\n          </IconButton>\n          <IconButton onClick={onNext}>\n            <SkipNextIcon />\n          </IconButton>\n          <IconButton\n            onClick={onRepeat}\n            className={clsx(s.iconButton, isRepeat !== 'off' && s.active)}>\n            {isRepeat === 'one' ? <IconOneRepeat /> : <RepeatIcon />}\n          </IconButton>\n        </div>\n\n        <div className={s.progressBar}>\n          <span className={s.time}>{format(currentTime)}</span>\n          <input\n            type=\"range\"\n            min={0}\n            max={duration}\n            value={currentTime}\n            onChange={handleChangeTime}\n            className={clsx(s.progress, s.trackProgress)}\n          />\n          <span className={s.time}>{format(duration)}</span>\n        </div>\n      </div>\n\n      <div className={s.volumeColumn}>\n        <IconButton onClick={handleVolumeMute}>\n          {volume > 0 ? <VolumeIcon /> : <VolumeMuteIcon />}\n        </IconButton>\n        <input\n          type=\"range\"\n          min={0}\n          max={1}\n          step={0.01}\n          value={volume}\n          onChange={handleVolume}\n          className={clsx(s.progress, s.volumeProgress)}\n        />\n      </div>\n    </div>\n  )\n}\n\nconst format = (sec: number) => {\n  const m = Math.floor(sec / 60)\n  const s = Math.floor(sec % 60)\n  return `${m}:${s.toString().padStart(2, '0')}`\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/AudioPlayer/AudioPlayerSceleton/AudioPlayerSkeleton.module.css",
    "content": ".skeleton {\n  display: flex;\n  flex-direction: row;\n  justify-content: center;\n  grid-column: 1 / -1;\n}\n\n.mySection {\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  width: 100%;\n}\n\n.infoTrack {\n  display: flex;\n  flex-direction: row;\n  gap: 15px;\n}\n\n.titleArtist {\n  padding-top: 15px;\n}\n\n.PlayBar {\n  display: flex;\n  flex-direction: row;\n  gap: 55px;\n  align-items: center;\n  padding-bottom: 10px;\n}\n\n.volume {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  padding-bottom: 10px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/AudioPlayer/AudioPlayerSceleton/AudioPlayerSkeleton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { AudioPlayerSkeleton } from './AudioPlayerSkeleton.tsx'\n\nconst meta: Meta<typeof AudioPlayerSkeleton> = {\n  title: 'Player/AudioPlayerSkeleton',\n  component: AudioPlayerSkeleton,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof AudioPlayerSkeleton>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/AudioPlayer/AudioPlayerSceleton/AudioPlayerSkeleton.tsx",
    "content": "import React from 'react'\n\nimport { Skeleton } from '@/shared/components'\n\nimport s from './AudioPlayerSkeleton.module.css'\n\nexport const AudioPlayerSkeleton = () => {\n  return (\n    <div className={s.skeleton}>\n      <section className={s.mySection}>\n        <div className={s.infoTrack}>\n          <Skeleton width=\"100%\" height={100} />\n          <div className={s.titleArtist}>\n            <Skeleton width={100} height={30} />\n            <Skeleton width={100} height={20} />\n          </div>\n        </div>\n        <div>\n          <div className={s.PlayBar}>\n            <Skeleton width={200} height={20} />\n            <Skeleton circle width={50} height={50} />\n            <Skeleton width={200} height={20} />\n          </div>\n          <div>\n            <Skeleton width=\"100%\" height={16} />\n          </div>\n        </div>\n        <div className={s.volume}>\n          <Skeleton width={100} height={25} />\n        </div>\n      </section>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/AudioPlayer/index.ts",
    "content": "export * from './AudioPlayer.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Autocomplete/Autocomplete.module.css",
    "content": ".container {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.labelError {\n  color: var(--color-text-error);\n}\n\n.inputWrapper {\n  position: relative;\n\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  align-items: flex-start;\n\n  min-height: 48px;\n  padding: 4px 32px 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.inputWrapper:hover:not(.disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.inputWrapper.focused {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-primary);\n}\n\n.inputWrapper.error {\n  border-color: var(--color-text-error);\n}\n\n.inputWrapper.disabled {\n  cursor: not-allowed;\n  background-color: var(--color-disabled);\n}\n\n.tag {\n  cursor: pointer;\n\n  display: flex;\n  gap: 4px;\n  align-items: center;\n  justify-content: center;\n\n  max-width: 120px;\n  padding: 2px 8px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 16px;\n\n  background-color: #2f2f2f;\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  overflow: hidden;\n\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.inputContainer {\n  display: flex;\n  gap: 5px;\n  align-items: center;\n\n  width: 100%;\n  min-height: 40px;\n  padding: 0 12px;\n  border: 1px solid #b3b3b3;\n  border-radius: 4px;\n\n  transition: border-color 0.2s ease;\n}\n\n.input {\n  flex: 1;\n\n  border: none;\n\n  font-family: inherit;\n  font-size: 14px;\n  color: var(--color-text-primary);\n\n  background: transparent;\n  outline: none;\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.dropdownIcon {\n  cursor: pointer;\n\n  position: absolute;\n  z-index: 2;\n  right: 8px;\n  bottom: 4px;\n\n  width: 20px;\n  height: 20px;\n\n  color: var(--color-text-secondary);\n\n  transition: transform 200ms ease;\n}\n\n.dropdownIcon:hover {\n  color: var(--color-text-primary);\n}\n\n.dropdownIconOpen {\n  transform: rotate(180deg);\n}\n\n.dropdown {\n  position: absolute;\n  z-index: 50;\n  top: 100%;\n  left: 0;\n\n  overflow-y: auto;\n\n  width: 100%;\n  max-height: 200px;\n  margin-top: 4px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 4px;\n\n  background-color: #2d2d2d;\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n\n  animation: dropdown-show 200ms ease-out;\n}\n\n.option {\n  cursor: pointer;\n\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  padding: 8px 12px;\n\n  transition: all 200ms ease;\n}\n\n.optionFocused:not(.optionDisabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.optionDisabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.noResults {\n  padding: 12px;\n  text-align: center;\n}\n\n.noResultsText {\n  color: var(--color-text-secondary);\n}\n\n.errorMessage {\n  margin-top: 4px;\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.counter {\n  margin-top: 4px;\n  color: var(--color-text-secondary);\n}\n\n/* Animations */\n@keyframes dropdown-show {\n  from {\n    transform: translateY(-4px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n.selected {\n  background: #51173c;\n}\n\n.tagsWrapper {\n  cursor: default;\n\n  position: relative;\n\n  display: flex;\n  flex: 1;\n  flex-wrap: nowrap;\n  gap: 4px;\n  align-content: flex-start;\n\n  min-width: 90%;\n  min-height: 24px;\n}\n\n.underlinedPart {\n  cursor: pointer;\n\n  display: inline;\n\n  font: inherit;\n  color: inherit;\n  text-decoration: underline;\n\n  background: none;\n}\n\n.hiddenTagsBlock {\n  position: absolute;\n  z-index: 10;\n  top: -50px;\n  left: 0;\n\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n\n  margin-top: 4px;\n  padding: 8px;\n  border: 1px solid var(--color-text-secondary);\n  border-radius: 4px;\n\n  background: black;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Autocomplete/Autocomplete.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from '../Dialog'\nimport { Typography } from '../Typography'\nimport { Autocomplete, type AutocompleteOption } from './Autocomplete'\n\nconst meta = {\n  title: 'Components/Autocomplete',\n  component: Autocomplete,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Autocomplete>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n// Sample data\nconst programmingLanguages: AutocompleteOption[] = [\n  { value: 'javascript', label: 'JavaScript' },\n  { value: 'typescript', label: 'TypeScript' },\n  { value: 'python', label: 'Python' },\n  { value: 'java', label: 'Java' },\n  { value: 'cpp', label: 'C++' },\n  { value: 'csharp', label: 'C#' },\n  { value: 'php', label: 'PHP' },\n  { value: 'ruby', label: 'Ruby' },\n  { value: 'go', label: 'Go' },\n  { value: 'rust', label: 'Rust' },\n  { value: 'kotlin', label: 'Kotlin' },\n  { value: 'swift', label: 'Swift' },\n]\n\nconst musicGenres: AutocompleteOption[] = [\n  { value: 'rock', label: 'Rock' },\n  { value: 'pop', label: 'Pop' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hiphop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n  { value: 'blues', label: 'Blues' },\n  { value: 'reggae', label: 'Reggae' },\n  { value: 'folk', label: 'Folk' },\n  { value: 'metal', label: 'Metal' },\n  { value: 'indie', label: 'Indie' },\n]\n\nconst skills: AutocompleteOption[] = [\n  { value: 'frontend', label: 'Frontend Development' },\n  { value: 'backend', label: 'Backend Development' },\n  { value: 'fullstack', label: 'Full Stack Development' },\n  { value: 'mobile', label: 'Mobile Development' },\n  { value: 'devops', label: 'DevOps' },\n  { value: 'testing', label: 'Testing & QA' },\n  { value: 'design', label: 'UI/UX Design' },\n  { value: 'pm', label: 'Project Management', disabled: true },\n  { value: 'data', label: 'Data Science' },\n  { value: 'ml', label: 'Machine Learning' },\n]\n\nexport const Basic = {\n  render: () => {\n    const [search, setSearch] = useState<string>('')\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Programming Languages\"\n          placeholder=\"Search and select languages...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [search, setSearch] = useState<string>('')\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (max 3)\"\n          placeholder=\"Choose up to 3 genres...\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          maxTags={3}\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithPreselected = {\n  render: () => {\n    const [search, setSearch] = useState<string>('')\n    const [selectedValues, setSelectedValues] = useState<string[]>(['javascript', 'typescript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Your Skills\"\n          placeholder=\"Add more skills...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithDisabledOptions = {\n  render: () => {\n    const [search, setSearch] = useState<string>('')\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Skills & Roles\"\n          placeholder=\"Select your skills...\"\n          options={skills}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [search, setSearch] = useState<string>('')\n    const [selectedValues, setSelectedValues] = useState<string[]>(['rock', 'jazz'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (disabled)\"\n          placeholder=\"Cannot select\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          disabled\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithError = {\n  render: () => {\n    const [search, setSearch] = useState<string>('')\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Required Skills\"\n          placeholder=\"Select at least one skill...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          errorMessage=\"Please select at least one programming language\"\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [backendSkills, setBackendSkills] = useState<string[]>([])\n    const [frontendSkills, setFrontendSkills] = useState<string[]>(['javascript'])\n    const [genres, setGenres] = useState<string[]>([])\n    const [search, setSearch] = useState<string>('')\n\n    const frontendOptions: AutocompleteOption[] = [\n      { value: 'html', label: 'HTML' },\n      { value: 'css', label: 'CSS' },\n      { value: 'javascript', label: 'JavaScript' },\n      { value: 'typescript', label: 'TypeScript' },\n      { value: 'react', label: 'React' },\n      { value: 'vue', label: 'Vue.js' },\n      { value: 'angular', label: 'Angular' },\n      { value: 'svelte', label: 'Svelte' },\n    ]\n\n    const backendOptions: AutocompleteOption[] = [\n      { value: 'nodejs', label: 'Node.js' },\n      { value: 'python', label: 'Python' },\n      { value: 'java', label: 'Java' },\n      { value: 'csharp', label: 'C#' },\n      { value: 'php', label: 'PHP' },\n      { value: 'ruby', label: 'Ruby' },\n      { value: 'go', label: 'Go' },\n      { value: 'rust', label: 'Rust' },\n    ]\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Developer Profile Setup\n          </Typography>\n        </div>\n\n        <Autocomplete\n          label=\"Frontend Technologies\"\n          placeholder=\"Select frontend skills...\"\n          options={frontendOptions}\n          value={frontendSkills}\n          onChange={setFrontendSkills}\n          maxTags={5}\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n\n        <Autocomplete\n          label=\"Backend Technologies\"\n          placeholder=\"Select backend skills...\"\n          options={backendOptions}\n          value={backendSkills}\n          onChange={setBackendSkills}\n          maxTags={4}\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n\n        <Autocomplete\n          label=\"Favorite Music Genres\"\n          placeholder=\"What music do you like?\"\n          options={musicGenres}\n          value={genres}\n          onChange={setGenres}\n          maxTags={6}\n          searchTerm={search}\n          setSearchTerm={setSearch}\n        />\n\n        <Card style={{ padding: '16px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Profile Summary\n          </Typography>\n\n          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>\n            <Typography variant=\"body2\">\n              <strong>Frontend:</strong>{' '}\n              {frontendSkills.length > 0 ? frontendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Backend:</strong>{' '}\n              {backendSkills.length > 0 ? backendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Music:</strong> {genres.length > 0 ? genres.join(', ') : 'None'}\n            </Typography>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => {\n    const [search1, setSearch1] = useState<string>('')\n    const [search2, setSearch2] = useState<string>('')\n    const [search3, setSearch3] = useState<string>('')\n    const [search4, setSearch4] = useState<string>('')\n\n    const [state1, setState1] = useState<string[]>([])\n    const [state2, setState2] = useState<string[]>(['rock', 'jazz'])\n    const [state3, setState3] = useState<string[]>([])\n    const [state4, setState4] = useState<string[]>(['javascript'])\n\n    return (\n      <div\n        style={{\n          width: '600px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '32px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Empty State\n          </Typography>\n          <Autocomplete\n            label=\"Programming Languages\"\n            placeholder=\"Start typing to search...\"\n            options={programmingLanguages}\n            value={state1}\n            onChange={setState1}\n            searchTerm={search1}\n            setSearchTerm={setSearch1}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Selected Values\n          </Typography>\n          <Autocomplete\n            label=\"Music Genres\"\n            placeholder=\"Add more genres...\"\n            options={musicGenres}\n            value={state2}\n            onChange={setState2}\n            searchTerm={search2}\n            setSearchTerm={setSearch2}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Error\n          </Typography>\n          <Autocomplete\n            label=\"Required Field\"\n            placeholder=\"This field is required\"\n            options={programmingLanguages}\n            value={state3}\n            onChange={setState3}\n            errorMessage=\"Please select at least one option\"\n            searchTerm={search3}\n            setSearchTerm={setSearch3}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Disabled State\n          </Typography>\n          <Autocomplete\n            label=\"Locked Selection\"\n            placeholder=\"Cannot modify\"\n            options={programmingLanguages}\n            value={state4}\n            onChange={setState4}\n            disabled\n            searchTerm={search4}\n            setSearchTerm={setSearch4}\n          />\n        </div>\n      </div>\n    )\n  },\n}\n\nexport const InDialog = {\n  render: () => {\n    const [isOpen, setIsOpen] = useState(false)\n    const [search, setSearch] = useState<string>('')\n    const [selectedGenres, setSelectedGenres] = useState<string[]>(['rock'])\n    const [selectedSkills, setSelectedSkills] = useState<string[]>([])\n\n    const handleSubmit = () => {\n      console.log('Selected skills:', selectedSkills)\n      console.log('Selected genres:', selectedGenres)\n      setIsOpen(false)\n    }\n\n    const handleReset = () => {\n      setSelectedSkills([])\n      setSelectedGenres([])\n    }\n\n    return (\n      <>\n        <Button onClick={() => setIsOpen(true)}>Open Profile Settings</Button>\n\n        <Dialog open={isOpen} onClose={() => setIsOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Edit Your Profile</Typography>\n            <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n              Update your skills and music preferences\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '24px',\n                minWidth: '400px',\n              }}>\n              <Autocomplete\n                label=\"Technical Skills\"\n                placeholder=\"Search and select your skills...\"\n                options={skills}\n                value={selectedSkills}\n                onChange={setSelectedSkills}\n                maxTags={8}\n                isRenderInPortal\n                searchTerm={search}\n                setSearchTerm={setSearch}\n              />\n\n              <Autocomplete\n                label=\"Favorite Music Genres\"\n                placeholder=\"What music do you enjoy?\"\n                options={musicGenres}\n                value={selectedGenres}\n                onChange={setSelectedGenres}\n                maxTags={5}\n                isRenderInPortal\n                searchTerm={search}\n                setSearchTerm={setSearch}\n              />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset All\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setIsOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={handleSubmit}>\n              Save Profile\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Autocomplete/Autocomplete.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { useGetId } from '@/shared/hooks'\nimport {\n  ArrowDownIcon,\n  SearchIcon,\n  CheckedIcon,\n  UncheckedIcon,\n  DeleteTagIconButton,\n} from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './Autocomplete.module.css'\nimport { useTranslation } from 'react-i18next'\n\nexport type AutocompleteOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type AutocompleteProps = {\n  label?: ReactNode\n  placeholder?: string\n  options: AutocompleteOption[]\n  value: string[]\n  searchTerm?: string\n  setSearchTerm?: (value: string) => void\n  onChange: (value: string[]) => void\n  disabled?: boolean\n  maxTags?: number\n  errorMessage?: string\n  className?: string\n  isRenderInPortal?: boolean\n} & Omit<ComponentProps<'div'>, 'onChange'>\n\nexport const Autocomplete = ({\n  label,\n  placeholder,\n  options,\n  value,\n  searchTerm: externalSearchTerm,\n  setSearchTerm: externalSetSearchTerm,\n  onChange,\n  disabled = false,\n  maxTags,\n  errorMessage,\n  className,\n  isRenderInPortal = false,\n  ...props\n}: AutocompleteProps) => {\n  const { t } = useTranslation()\n  const [internalSearchTerm, setInternalSearchTerm] = useState('')\n  const [isOpen, setIsOpen] = useState(false)\n  const [focusedIndex, setFocusedIndex] = useState(-1)\n  const [isPopupOpen, setIsPopupOpen] = useState(false)\n  const hiddenTagsBlockRef = useRef<HTMLDivElement>(null)\n\n  const searchTerm = externalSearchTerm !== undefined ? externalSearchTerm : internalSearchTerm\n  const setSearchTerm = externalSetSearchTerm || setInternalSearchTerm\n\n  const containerRef = useRef<HTMLDivElement>(null)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const inputWrapperRef = useRef<HTMLDivElement>(null)\n  const dropdownRef = useRef<HTMLDivElement | null>(null)\n\n  const id = useGetId(props.id)\n\n  const filteredOptions = options\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n  const showError = Boolean(errorMessage)\n\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as Node\n      if (containerRef.current && !containerRef.current.contains(target)) {\n        if (isRenderInPortal) {\n          if (\n            inputWrapperRef.current &&\n            !inputWrapperRef.current.contains(target) &&\n            dropdownRef.current &&\n            !dropdownRef.current.contains(target)\n          ) {\n            setIsOpen(false)\n            setFocusedIndex(-1)\n          }\n        } else {\n          setIsOpen(false)\n          setFocusedIndex(-1)\n        }\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen, isRenderInPortal])\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (disabled) return\n\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault()\n        if (!isOpen) {\n          setIsOpen(true)\n          setFocusedIndex(0)\n        } else {\n          setFocusedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : prev))\n        }\n        break\n\n      case 'ArrowUp':\n        e.preventDefault()\n        setFocusedIndex((prev) => (prev > 0 ? prev - 1 : 0))\n        break\n\n      case 'Enter':\n        e.preventDefault()\n        if (isOpen && focusedIndex >= 0 && filteredOptions[focusedIndex]) {\n          toggleOption(filteredOptions[focusedIndex])\n        }\n        break\n\n      case 'Escape':\n        e.preventDefault()\n        setIsOpen(false)\n        setFocusedIndex(-1)\n        break\n    }\n  }\n\n  const toggleOption = (option: AutocompleteOption) => {\n    if (option.disabled) return\n\n    const isSelected = value.includes(option.value)\n\n    if (isSelected) {\n      onChange(value.filter((v) => v !== option.value))\n    } else if (!isMaxTagsReached) {\n      onChange([...value, option.value])\n    }\n  }\n\n  const removeTag = (tagValue: string) => {\n    onChange(value.filter((v) => v !== tagValue))\n  }\n\n  const handleInputFocus = () => {\n    if (!disabled) {\n      setIsOpen(true)\n    }\n  }\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setSearchTerm(e.target.value)\n    setIsOpen(true)\n    setFocusedIndex(-1)\n  }\n\n  const selectedOptions = options.filter((option) => value.includes(option.value))\n\n  const maxVisibleTags = 2\n  const visibleTags = selectedOptions.slice(0, maxVisibleTags)\n  const hiddenTagsCount = selectedOptions.length - maxVisibleTags\n  const hiddenTags = selectedOptions.slice(maxVisibleTags)\n\n  useEffect(() => {\n    if (isPopupOpen && hiddenTagsBlockRef.current) {\n      hiddenTagsBlockRef.current.focus()\n    }\n  }, [isPopupOpen])\n\n  return (\n    <div className={clsx(s.container, className)} ref={containerRef} {...props}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          className={clsx(s.label, showError && s.labelError)}\n          as=\"label\"\n          htmlFor={id}>\n          {label}\n        </Typography>\n      )}\n\n      <div\n        className={clsx(\n          s.inputWrapper,\n          isOpen && s.focused,\n          showError && s.error,\n          disabled && s.disabled\n        )}\n        ref={inputWrapperRef}>\n        <div className={s.tagsWrapper}>\n          {visibleTags.map((option) => (\n            <div key={option.value} className={s.tag} title={option.label}>\n              <Typography variant=\"body2\" className={s.tagText} as=\"label\">\n                #{option.label}\n              </Typography>\n\n              {!disabled && (\n                <IconButton\n                  onClick={() => removeTag(option.value)}\n                  className={s.deleteButton}\n                  type=\"button\"\n                  tabIndex={-1}>\n                  <DeleteTagIconButton />\n                </IconButton>\n              )}\n            </div>\n          ))}\n\n          {hiddenTagsCount > 0 && (\n            <div className={s.hidenTags}>\n              <Typography variant=\"body2\" className={s.tagText}>\n                and{' '}\n                <button\n                  className={s.underlinedPart}\n                  onClick={(e) => {\n                    e.stopPropagation()\n                    setIsPopupOpen(!isPopupOpen)\n                  }}>\n                  {hiddenTagsCount} more\n                </button>\n              </Typography>\n            </div>\n          )}\n        </div>\n        {isPopupOpen && hiddenTagsCount > 0 && (\n          <div\n            className={s.hiddenTagsBlock}\n            ref={hiddenTagsBlockRef}\n            tabIndex={0}\n            autoFocus\n            onBlur={(e) => {\n              if (!e.currentTarget.contains(e.relatedTarget as Node)) {\n                setIsPopupOpen(false)\n              }\n            }}\n            onKeyDown={(e) => {\n              if (e.key === 'Escape') {\n                setIsPopupOpen(false)\n              }\n            }}>\n            {hiddenTags.map((option) => (\n              <div key={option.value} className={s.tag} title={option.label}>\n                <Typography variant=\"body2\" className={s.tagText} as=\"label\">\n                  #{option.label}\n                </Typography>\n\n                {!disabled && (\n                  <IconButton\n                    onClick={() => removeTag(option.value)}\n                    className={s.deleteButton}\n                    type=\"button\"\n                    tabIndex={-1}>\n                    <DeleteTagIconButton />\n                  </IconButton>\n                )}\n              </div>\n            ))}\n          </div>\n        )}\n\n        <div className={s.inputContainer}>\n          <SearchIcon width={20} height={20} />\n          <input\n            id={id}\n            ref={inputRef}\n            type=\"text\"\n            className={s.input}\n            value={searchTerm}\n            onChange={handleInputChange}\n            onFocus={handleInputFocus}\n            onKeyDown={handleKeyDown}\n            placeholder={placeholder || t('placeholder.search_and_select')}\n            disabled={disabled || isMaxTagsReached}\n            autoComplete=\"off\"\n          />\n        </div>\n        <ArrowDownIcon\n          className={clsx(s.dropdownIcon, isOpen && s.dropdownIconOpen)}\n          onClick={() => !disabled && setIsOpen(!isOpen)}\n        />\n      </div>\n\n      {isRenderInPortal ? (\n        <AutocompleteDropdownPortal\n          anchorRef={inputWrapperRef}\n          dropdownRef={dropdownRef}\n          isOpen={isOpen && !disabled}>\n          <div className={s.dropdown}>\n            {filteredOptions.length > 0 ? (\n              filteredOptions.map((option, index) => {\n                const isSelected = value.includes(option.value)\n\n                return (\n                  <div\n                    key={option.value}\n                    role=\"option\"\n                    aria-selected={isSelected}\n                    aria-disabled={option.disabled}\n                    className={clsx(\n                      s.option,\n                      index === focusedIndex && s.optionFocused,\n                      option.disabled && s.optionDisabled,\n                      isSelected && s.selected\n                    )}\n                    onMouseEnter={() => setFocusedIndex(index)}\n                    onMouseDown={(e) => e.preventDefault()}\n                    onClick={() => !option.disabled && toggleOption(option)}\n                    onMouseLeave={() => setFocusedIndex(-1)}>\n                    {isSelected ? <CheckedIcon /> : <UncheckedIcon />}\n                    <Typography variant=\"body2\">#{option.label}</Typography>\n                  </div>\n                )\n              })\n            ) : (\n              <div className={s.noResults}>\n                <Typography variant=\"body2\" className={s.noResultsText}>\n                  {searchTerm\n                    ? t('placeholder.no_options_found')\n                    : t('placeholder.all_options_selected')}\n                </Typography>\n              </div>\n            )}\n          </div>\n        </AutocompleteDropdownPortal>\n      ) : (\n        isOpen &&\n        !disabled && (\n          <div className={s.dropdown}>\n            {filteredOptions.length > 0 ? (\n              filteredOptions.map((option, index) => {\n                const isSelected = value.includes(option.value)\n\n                return (\n                  <div\n                    key={option.value}\n                    role=\"option\"\n                    aria-selected={isSelected}\n                    aria-disabled={option.disabled}\n                    className={clsx(\n                      s.option,\n                      index === focusedIndex && s.optionFocused,\n                      option.disabled && s.optionDisabled,\n                      isSelected && s.selected\n                    )}\n                    onMouseEnter={() => setFocusedIndex(index)}\n                    onMouseDown={(e) => e.preventDefault()}\n                    onClick={() => !option.disabled && toggleOption(option)}\n                    onMouseLeave={() => setFocusedIndex(-1)}>\n                    {isSelected ? <CheckedIcon /> : <UncheckedIcon />}\n                    <Typography variant=\"body2\">#{option.label}</Typography>\n                  </div>\n                )\n              })\n            ) : (\n              <div className={s.noResults}>\n                <Typography variant=\"body2\" className={s.noResultsText}>\n                  {searchTerm\n                    ? t('placeholder.no_options_found')\n                    : t('placeholder.all_options_selected')}\n                </Typography>\n              </div>\n            )}\n          </div>\n        )\n      )}\n\n      {showError && (\n        <Typography variant=\"error\" className={s.errorMessage}>\n          {errorMessage}\n        </Typography>\n      )}\n\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} {t('placeholder.selected')}\n        </Typography>\n      )}\n    </div>\n  )\n}\n\ntype AutocompleteDropdownPortalProps = {\n  anchorRef: React.RefObject<HTMLElement | null>\n  dropdownRef: React.RefObject<HTMLDivElement | null>\n  children: ReactNode\n  isOpen: boolean\n}\n\nconst AutocompleteDropdownPortal = ({\n  anchorRef,\n  dropdownRef,\n  children,\n  isOpen,\n}: AutocompleteDropdownPortalProps) => {\n  const [styles, setStyles] = useState<{ top: number; left: number; width: number }>({\n    top: 0,\n    left: 0,\n    width: 0,\n  })\n\n  useEffect(() => {\n    if (isOpen && anchorRef.current) {\n      const rect = anchorRef.current.getBoundingClientRect()\n      setStyles({\n        top: rect.bottom + window.scrollY,\n        left: rect.left + window.scrollX,\n        width: rect.width,\n      })\n    }\n  }, [isOpen, anchorRef])\n\n  if (!isOpen) return null\n\n  return createPortal(\n    <div\n      ref={dropdownRef}\n      style={{\n        position: 'absolute',\n        top: styles.top,\n        left: styles.left,\n        width: styles.width,\n        zIndex: 9999,\n      }}>\n      {children}\n    </div>,\n    document.body\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Autocomplete/index.ts",
    "content": "export * from './Autocomplete'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Avatar/Avatar.module.css",
    "content": ".avatar {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 192px;\n  height: 192px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-xl);\n\n  background-color: rgb(15 15 15 / 80%);\n}\n\n.avatar > img {\n  border-radius: 50%;\n}\n\n.initials {\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Avatar/Avatar.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Avatar } from './Avatar'\n\nconst meta = {\n  title: 'Components/Avatar',\n  component: Avatar,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Avatar>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const LoginInitial: Story = {\n  args: {},\n}\n\nexport const InitialsOnly: Story = {\n  args: {\n    fullName: { name: 'james', surname: 'allen' },\n  },\n}\n\nexport const ProfileImage: Story = {\n  args: {\n    src: 'https://unsplash.it/192/192',\n    fullName: { name: 'james', surname: 'allen' },\n    userLogin: 'james',\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Avatar/Avatar.tsx",
    "content": "import { clsx } from 'clsx'\n\nimport type { FullName } from '@/features/profile'\nimport { getUserInitials } from '@/shared/utils'\n\nimport s from './Avatar.module.css'\n\ntype DefaultAvatarProps = {\n  src?: string | null\n  fullName?: FullName\n  userLogin?: string\n  className?: string\n}\n\nexport const Avatar = ({ src, fullName, userLogin, className }: DefaultAvatarProps) => {\n  const classNames = clsx(s.avatar, className)\n\n  const initials = getUserInitials(fullName, userLogin)\n\n  return (\n    <div className={classNames}>\n      {src ? <img src={src} alt=\"User avatar\" /> : <span className={s.initials}>{initials}</span>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Avatar/index.ts",
    "content": "export * from './Avatar'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Button/Button.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: inline-flex;\n  gap: 4px;\n  align-items: center;\n  justify-content: center;\n\n  height: 40px;\n  padding: 8px 16px;\n  border-radius: 45px;\n\n  font-size: var(--font-size-s);\n  font-weight: 600;\n  color: var(--color-text-primary);\n\n  transition: opacity 200ms;\n}\n\n.button:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.button:disabled {\n  cursor: initial;\n  opacity: 0.5;\n  background-color: var(--color-disabled);\n}\n\n.button:hover:not(:disabled),\n.button:focus:not(:disabled) {\n  opacity: 0.8;\n}\n\n.primary {\n  background-color: var(--color-accent);\n}\n\n.secondary {\n  background-color: var(--color-bg-interactive-secondary);\n}\n\n.fullWidth {\n  width: 100%;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Button/Button.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Button } from './Button'\n\nconst meta = {\n  title: 'Components/Button',\n  component: Button,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Button>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllButtons: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '24px',\n        flexDirection: 'column',\n        alignItems: 'center',\n        width: '250px',\n      }}>\n      <Button variant=\"primary\">Primary</Button>\n      <Button variant=\"secondary\">Secondary</Button>\n      <Button fullWidth>Full Width</Button>\n      <Button disabled>Disabled</Button>\n      <Button variant=\"primary\" as=\"p\" href=\"https://it-incubator.io/\" target=\"_blank\">\n        Link\n      </Button>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Button/Button.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Button.module.css'\n\nexport type ButtonVariant = 'primary' | 'secondary'\n\nexport type ButtonProps<T extends ElementType = 'button'> = {\n  as?: T\n  fullWidth?: boolean\n  variant?: ButtonVariant\n} & ComponentProps<T>\n\nexport const Button = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  className,\n  fullWidth = false,\n  variant = 'primary',\n  ...props\n}: ButtonProps<T>) => {\n  const classNames = clsx(s.button, s[variant], fullWidth && s.fullWidth, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Button/index.ts",
    "content": "export * from './Button'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Card/Card.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n\n  height: 100%;\n  padding: 8px;\n\n  background: var(--color-bg-card);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Card/Card.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from '../Typography'\nimport { Card } from './Card'\n\nconst meta = {\n  title: 'Components/Card',\n  component: Card,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Card>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  render: () => (\n    <Card>\n      <Typography variant=\"h2\">Chill Mix</Typography>\n      <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n        Julia Wolf, Khalid, ayokay and more\n      </Typography>\n    </Card>\n  ),\n}\n\nexport const AsSection: Story = {\n  render: () => (\n    <Card as=\"section\">\n      <Typography variant=\"h3\">Card as section</Typography>\n      <Typography variant=\"caption\">You can use any tag via 'as' prop</Typography>\n    </Card>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Card/Card.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType, ReactNode } from 'react'\n\nimport s from './Card.module.css'\n\nexport type CardProps<T extends ElementType = 'div'> = {\n  as?: T\n  className?: string\n  children?: ReactNode\n} & ComponentProps<T>\n\nexport const Card = <T extends ElementType = 'div'>({\n  as: Component = 'div',\n  className,\n  children,\n  ...props\n}: CardProps<T>) => {\n  return (\n    <Component className={clsx(s.card, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Card/index.ts",
    "content": "export * from './Card'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Dialog/Dialog.module.css",
    "content": ".backdrop {\n  position: fixed;\n  z-index: 1;\n  inset: 0;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  background-color: rgb(0 0 0 / 50%);\n\n  animation: fade-in 200ms ease-out;\n}\n\n.dialog {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n\n  max-width: 745px;\n  max-height: 90vh;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-secondary);\n\n  animation: slide-in 200ms ease-out;\n}\n\n.header {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  padding: 18px 24px;\n}\n\n.closeButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: 16px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.closeButton:hover {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  overflow-y: auto;\n  flex: 1;\n  padding: 20px 24px;\n}\n\n.footer {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 8px;\n  padding: 18px 24px;\n}\n\n/* Animations */\n@keyframes fade-in {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes slide-in {\n  from {\n    transform: translateY(-500px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n/* Responsive */\n@media (width <= 768px) {\n  .dialog {\n    max-width: 95vw;\n    margin: 20px;\n  }\n\n  .header,\n  .content,\n  .footer {\n    padding-right: 16px;\n    padding-left: 16px;\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Dialog/Dialog.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from './index'\n\nconst meta = {\n  title: 'Components/Dialog',\n  component: Dialog,\n  parameters: {\n    layout: 'centered',\n  },\n} satisfies Meta<typeof Dialog>\n\nexport default meta\n\nexport const BasicDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Open Basic Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Dialog Title</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <Typography variant=\"body1\">\n              This is dialog content. Here can be any content - text, forms, images and much more.\n            </Typography>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Confirm\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const FormDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Form Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Sign in to Spotifun</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '16px',\n                minWidth: '320px',\n              }}>\n              <TextField label=\"Email or username\" placeholder=\"Enter email or username\" />\n              <TextField label=\"Password\" type=\"password\" placeholder=\"Enter password\" />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Continue without signing in\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Sign in with API/HUB\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const WithoutCloseButton = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog without close button</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader showCloseButton={false}>\n            <Typography variant=\"h2\">Millions of songs.</Typography>\n            <Typography variant=\"body1\" style={{ color: 'var(--color-text-secondary)' }}>\n              Free on Musicfun.\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ textAlign: 'center', padding: '20px 0' }}>\n              <div\n                style={{\n                  width: '60px',\n                  height: '60px',\n                  borderRadius: '50%',\n                  backgroundColor: 'var(--color-accent)',\n                  margin: '0 auto 16px',\n                  display: 'flex',\n                  alignItems: 'center',\n                  justifyContent: 'center',\n                  fontSize: '24px',\n                }}>\n                😊\n              </div>\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '12px',\n                width: '100%',\n              }}>\n              <Button variant=\"primary\" fullWidth onClick={() => setOpen(false)}>\n                Sign up with API/HUB\n              </Button>\n              <Button variant=\"secondary\" fullWidth onClick={() => setOpen(false)}>\n                Continue without signing in\n              </Button>\n            </div>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const LongContent = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog with long content</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Long Content</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ maxWidth: '500px' }}>\n              {Array.from({ length: 20 }, (_, i) => (\n                <Typography key={i} variant=\"body2\" style={{ marginBottom: '12px' }}>\n                  This is paragraph number {i + 1}. Lorem ipsum dolor sit amet, consectetur\n                  adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna\n                  aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\n                </Typography>\n              ))}\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Close\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Dialog/Dialog.tsx",
    "content": "import { clsx } from 'clsx'\nimport { createContext, type ReactNode, use, useEffect } from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { IconButton } from '../IconButton'\nimport s from './Dialog.module.css'\n\ntype DialogContextType = {\n  onClose?: () => void\n}\n\nconst DialogContext = createContext<DialogContextType | null>(null)\n\nconst useDialogContext = () => {\n  const context = use(DialogContext)\n  if (!context) {\n    throw new Error('Dialog compound components must be used within Dialog component')\n  }\n  return context\n}\n\nexport type DialogProps = {\n  children: ReactNode\n  open: boolean\n  onClose?: () => void\n  className?: string\n}\n\nexport const Dialog = ({ children, open, onClose, className }: DialogProps) => {\n  const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (e.target === e.currentTarget) {\n      onClose?.()\n    }\n  }\n\n  // Add global keydown handler for ESC key\n  useEffect(() => {\n    if (!open) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose?.()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [open, onClose])\n\n  if (!open) return null\n\n  const dialogContent = (\n    <div className={s.backdrop} onClick={handleBackdropClick} role=\"dialog\" aria-modal=\"true\">\n      <section className={clsx(s.dialog, className)}>\n        <DialogContext value={{ onClose }}>{children}</DialogContext>\n      </section>\n    </div>\n  )\n\n  return createPortal(dialogContent, document.body)\n}\n\n/*\n * DialogHeader\n */\n\nexport type DialogHeaderProps = {\n  children?: ReactNode\n  className?: string\n  showCloseButton?: boolean\n}\n\nexport const DialogHeader = ({\n  children,\n  className,\n  showCloseButton = true,\n}: DialogHeaderProps) => {\n  const { onClose } = useDialogContext()\n\n  return (\n    <header className={clsx(s.header, className)}>\n      <div>{children}</div>\n      {showCloseButton && (\n        <IconButton onClick={onClose} aria-label=\"Close dialog\" type=\"button\">\n          ✕\n        </IconButton>\n      )}\n    </header>\n  )\n}\n\n/*\n * DialogContent\n */\n\nexport type DialogContentProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogContent = ({ children, className }: DialogContentProps) => {\n  return <div className={clsx(s.content, className)}>{children}</div>\n}\n\n/*\n * DialogFooter\n */\n\nexport type DialogFooterProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogFooter = ({ children, className }: DialogFooterProps) => {\n  return <footer className={clsx(s.footer, className)}>{children}</footer>\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Dialog/index.ts",
    "content": "export * from './Dialog'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/DropdownMenu/DropdownMenu.module.css",
    "content": ".container {\n  position: relative;\n  display: inline-block;\n}\n\n.trigger {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.trigger:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.trigger:enabled:hover,\n.trigger:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  z-index: 50;\n\n  min-width: 160px;\n  padding: 4px;\n  border-radius: 8px;\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n\n  /* HeadlessUI transition classes */\n}\n\n.content[data-closed] {\n  transform: scale(0.95);\n  opacity: 0;\n}\n\n.content[data-open] {\n  transform: scale(1);\n  opacity: 1;\n}\n\n/* Alignment styles for HeadlessUI positioning */\n.content.align-start {\n  transform-origin: top left;\n}\n\n.content.align-center {\n  transform-origin: top center;\n}\n\n.content.align-end {\n  transform-origin: top right;\n}\n\n.content.side-top {\n  transform-origin: bottom;\n}\n\n.item {\n  cursor: pointer;\n\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: none;\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-align: left;\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.item:focus,\n.item[data-focus] {\n  background-color: var(--color-accent);\n  outline: none;\n}\n\n.item:hover:not(:disabled) {\n  background-color: var(--color-accent);\n}\n\n.itemDisabled {\n  cursor: not-allowed;\n  color: var(--color-text-secondary);\n  opacity: 0.5;\n}\n\n.itemDisabled:hover {\n  background: transparent;\n}\n\n.separator {\n  height: 1px;\n  margin: 4px 0;\n  background-color: var(--color-border-base);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { CreateIcon, MoreIcon, PlusIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from './DropdownMenu'\n\nconst meta: Meta<typeof DropdownMenu> = {\n  title: 'Components/DropdownMenu',\n  component: DropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const BasicDropdownMenu: Story = {\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n          Show text song\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithIcons: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist')}>\n          <PlusIcon />\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithDisabledItem: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem disabled>Add to playlist (disabled)</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const CustomTrigger: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>Trigger 🐯</DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Action 1')}>Action 1</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 2')}>Action 2</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 3')}>Action 3</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithLinks: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          as=\"a\"\n          href=\"https://example.com\"\n          target=\"_blank\"\n          onClick={() => console.log('Link clicked')}>\n          <PlusIcon />\n          Visit Website\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/DropdownMenu/DropdownMenu.tsx",
    "content": "import { Menu, MenuButton, MenuItem, MenuItems, MenuSeparator } from '@headlessui/react'\nimport { clsx } from 'clsx'\nimport { type ComponentProps, type ElementType, type ReactNode } from 'react'\n\nimport s from './DropdownMenu.module.css'\n\n/*\n * DropdownMenu\n */\n\nexport type DropdownMenuProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DropdownMenu = ({ children, className }: DropdownMenuProps) => {\n  return (\n    <Menu as=\"div\" className={clsx(s.container, className)}>\n      {children}\n    </Menu>\n  )\n}\n\n/*\n * DropdownMenuTrigger\n */\n\nexport type DropdownMenuTriggerProps = {\n  children: ReactNode\n  className?: string\n  asChild?: boolean\n}\n\nexport const DropdownMenuTrigger = ({\n  children,\n  className,\n  asChild = false,\n}: DropdownMenuTriggerProps) => {\n  const handleClick = (event: React.MouseEvent) => {\n    event.preventDefault()\n    event.stopPropagation()\n  }\n\n  if (asChild) {\n    return (\n      <MenuButton as=\"div\" className={className} onClick={handleClick}>\n        {children}\n      </MenuButton>\n    )\n  }\n\n  return (\n    <MenuButton className={clsx(s.trigger, className)} onClick={handleClick}>\n      {children}\n    </MenuButton>\n  )\n}\n\n/*\n * DropdownMenuContent\n */\n\nexport type DropdownMenuContentProps = {\n  children: ReactNode\n  className?: string\n  align?: 'start' | 'center' | 'end'\n  side?: 'top' | 'bottom' | 'left' | 'right'\n}\n\nexport const DropdownMenuContent = ({\n  children,\n  className,\n  align = 'end',\n  side = 'bottom',\n}: DropdownMenuContentProps) => {\n  return (\n    <MenuItems\n      portal\n      anchor=\"bottom\"\n      className={clsx(s.content, s[`align-${align}`], s[`side-${side}`], className)}>\n      {children}\n    </MenuItems>\n  )\n}\n\n/*\n * DropdownMenuItem\n */\n\nexport type DropdownMenuItemProps<T extends ElementType = 'button'> = {\n  as?: T\n  children: ReactNode\n  onClick?: () => void\n  className?: string\n  disabled?: boolean\n} & ComponentProps<T>\n\nexport const DropdownMenuItem = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  onClick,\n  className,\n  disabled = false,\n  ...props\n}: DropdownMenuItemProps<T>) => {\n  const handleClick = (event: React.MouseEvent) => {\n    event.stopPropagation()\n    if (disabled) return\n    onClick?.()\n  }\n\n  return (\n    <MenuItem disabled={disabled}>\n      <Component\n        {...(Component === 'button' && { type: 'button' })}\n        className={clsx(s.item, disabled && s.itemDisabled, className)}\n        onClick={handleClick}\n        {...(Component === 'button' && { disabled })}\n        {...props}>\n        {children}\n      </Component>\n    </MenuItem>\n  )\n}\n\n/*\n * DropdownMenuSeparator\n */\n\nexport type DropdownMenuSeparatorProps = {\n  className?: string\n}\n\nexport const DropdownMenuSeparator = ({ className }: DropdownMenuSeparatorProps) => {\n  return <MenuSeparator className={clsx(s.separator, className)} />\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/DropdownMenu/index.ts",
    "content": "export * from './DropdownMenu'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/FileUploader/FileUploader.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  align-items: center;\n\n  width: 100%;\n}\n\n.uploadButton {\n  cursor: pointer;\n\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  justify-content: center;\n\n  min-width: 280px;\n  min-height: 64px;\n  padding: 0 32px;\n  border: none;\n  border-radius: 12px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-interactive-secondary);\n  box-shadow: 0 2px 16px 0 rgb(0 0 0 / 12%);\n\n  transition: background 0.2s;\n}\n\n.uploadButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.6;\n}\n\n.uploadButton:hover:not(:disabled),\n.uploadButton:focus-visible:not(:disabled) {\n  background: var(--color-bg-input-hover);\n}\n\n.icon {\n  width: 24px;\n  height: 24px;\n  color: var(--color-text-primary);\n}\n\n.input {\n  display: none;\n}\n\n.fileNameBox {\n  display: flex;\n  gap: 10px;\n  align-items: center;\n\n  min-width: 220px;\n  min-height: 40px;\n  margin-top: 8px;\n  padding: 0 20px;\n  border-radius: 10px;\n\n  font-size: var(--font-size-m);\n  font-weight: 400;\n  color: var(--color-text-primary);\n\n  background: var(--color-bg-secondary);\n  box-shadow: 0 1px 8px 0 rgb(0 0 0 / 8%);\n}\n\n.fileNameBox .icon {\n  width: 20px;\n  height: 20px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/FileUploader/FileUploader.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Typography } from '../Typography'\nimport { FileUploader } from './FileUploader'\n\nconst meta = {\n  title: 'Components/FileUploader',\n  component: FileUploader,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof FileUploader>\n\nexport default meta\n\nexport const Basic = {\n  render: () => {\n    const [file, setFile] = useState<File | null>(null)\n    return (\n      <div style={{ width: 400 }}>\n        <FileUploader onFileSelect={setFile} value={file} onRemove={() => setFile(null)} />\n        {file && (\n          <Typography variant=\"caption\" style={{ marginTop: 16, display: 'block' }}>\n            Selected: {file.name}\n          </Typography>\n        )}\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => <FileUploader onFileSelect={() => {}} disabled />,\n}\n\nexport const Loading = {\n  render: () => <FileUploader onFileSelect={() => {}} loading placeholder=\"Loading...\" />,\n}\n\nexport const Resettable = {\n  render: () => {\n    const [file, setFile] = useState<File | null>(null)\n    return (\n      <div style={{ width: 400 }}>\n        <FileUploader\n          onFileSelect={setFile}\n          value={file}\n          onRemove={() => setFile(null)}\n          placeholder=\"Choose Track\"\n        />\n        <button style={{ marginTop: 16 }} onClick={() => setFile(null)}>\n          Reset file (external controller)\n        </button>\n      </div>\n    )\n  },\n}\n\nexport const CustomPlaceholder = {\n  render: () => <FileUploader onFileSelect={() => {}} placeholder=\"Загрузить аудиофайл\" />,\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/FileUploader/FileUploader.tsx",
    "content": "import { t } from 'i18next'\nimport { useState } from 'react'\n\nimport { AddTrackIcon, DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './FileUploader.module.css'\n\ntype FileUploaderProps = {\n  onFileSelect: (file: File) => void\n  accept?: string\n  placeholder?: string\n  disabled?: boolean\n  loading?: boolean\n  value?: File | null // controlled value (optional)\n  onRemove?: () => void // optional remove handler\n}\n\nexport const FileUploader = ({\n  onFileSelect,\n  accept = '.mp3,audio/*',\n  placeholder = t('tracks.button.choose_track'),\n  disabled = false,\n  loading = false,\n  value,\n  onRemove,\n}: FileUploaderProps) => {\n  // Controlled/uncontrolled logic\n  const [internalFile, setInternalFile] = useState<File | null>(null)\n  const file = value !== undefined ? value : internalFile\n\n  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0]\n    if (file) {\n      setInternalFile(file)\n      onFileSelect(file)\n    }\n  }\n\n  const handleRemove = () => {\n    setInternalFile(null)\n    onRemove?.()\n  }\n\n  // If file is selected, show only the file name block\n  if (file) {\n    return (\n      <div className={s.container}>\n        <div className={s.fileNameBox}>\n          <AddTrackIcon className={s.icon} />\n          <Typography variant=\"body2\">{file.name}</Typography>\n          {onRemove && (\n            <IconButton\n              type=\"button\"\n              className={s.removeBtn}\n              onClick={handleRemove}\n              disabled={loading || disabled}>\n              <DeleteIcon width={20} height={20} />\n            </IconButton>\n          )}\n        </div>\n      </div>\n    )\n  }\n\n  // If file is not selected, show the select button\n  return (\n    <div className={s.container}>\n      <label className={s.uploadButton}>\n        <AddTrackIcon className={s.icon} />\n        <span>{placeholder}</span>\n        <input\n          type=\"file\"\n          accept={accept}\n          className={s.input}\n          onChange={handleFileChange}\n          disabled={disabled || loading}\n          tabIndex={-1}\n        />\n      </label>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/FileUploader/index.ts",
    "content": "export * from './FileUploader'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/FormControlledTextField/FormControlledTextField.tsx",
    "content": "import { type Control, type FieldPath, type FieldValues, useController } from 'react-hook-form'\n\nimport { TextField, type TextFieldProps } from '@/shared/components'\n\ntype FormControlledTextFieldProps<T extends FieldValues> = {\n  name: FieldPath<T>\n  control: Control<T>\n} & Omit<TextFieldProps, 'name' | 'value' | 'onChange' | 'onBlur' | 'ref'>\n\nexport const FormControlledTextField = <T extends FieldValues>({\n  name,\n  control,\n  ...rest\n}: FormControlledTextFieldProps<T>) => {\n  const {\n    field,\n    fieldState: { error },\n  } = useController({ name, control })\n\n  return <TextField {...field} {...rest} errorMessage={error?.message} />\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/FormControlledTextField/index.ts",
    "content": "export * from './FormControlledTextField'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Hashtag/Tag.module.css",
    "content": ".hashtag {\n  cursor: pointer;\n\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n\n  min-width: 73px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 45px;\n\n  font-size: var(--font-size-xxxs);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  text-decoration: none;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.hashtag:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.hashtag:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.active {\n  color: var(--color-bg-primary);\n  background-color: var(--color-text-primary);\n}\n\n.active:hover:not(:disabled) {\n  color: var(--color-bg-primary);\n  opacity: 0.9;\n  background-color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Hashtag/Tag.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Tag } from './Tag.tsx'\n\nconst meta = {\n  title: 'Components/Hashtag',\n  component: Tag,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    tag: 'Playlists',\n  },\n} satisfies Meta<typeof Tag>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n\nexport const Active: Story = {\n  args: {\n    active: true,\n  },\n}\n\nexport const AsLink: Story = {\n  args: {\n    as: 'a',\n    href: 'https://www.google.com',\n    target: '_blank',\n  },\n}\n\nexport const AllHashtags: Story = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '16px' }}>\n      <Tag tag=\"Playlists\" />\n      <Tag active tag=\"Artists\" />\n      <Tag tag=\"Albums\" />\n      <Tag as=\"a\" href=\"#\" tag=\"Podcasts & shows\">\n        Podcasts & shows\n      </Tag>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Hashtag/Tag.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Tag.module.css'\n\nexport type HashtagProps<T extends ElementType = 'button'> = {\n  as?: T\n  active?: boolean\n  tag: string\n  className?: string\n} & ComponentProps<T>\n\nexport const Tag = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  active = false,\n  tag,\n  className,\n  ...props\n}: HashtagProps<T>) => {\n  const classNames = clsx(s.hashtag, active && s.active, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      #{tag}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Hashtag/index.ts",
    "content": "export * from './Tag.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/IconButton/IconButton.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.button:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.button:enabled:hover,\n.button:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/IconButton/IconButton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport {\n  DownloadIcon,\n  HomeIcon,\n  LikeIcon,\n  MoreIcon,\n  PlayIcon,\n  PlusIcon,\n  SearchIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from './IconButton'\n\nconst meta = {\n  title: 'Components/IconButton',\n  component: IconButton,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof IconButton>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    children: <PlayIcon />,\n    'aria-label': 'Play',\n  },\n}\n\nexport const AllIcons = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '16px',\n        flexWrap: 'wrap',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: '20px',\n      }}>\n      <IconButton aria-label=\"Home\">\n        <HomeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Search\">\n        <SearchIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Play\">\n        <PlayIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Like\">\n        <LikeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Add\">\n        <PlusIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"More options\">\n        <MoreIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Download\">\n        <DownloadIcon />\n      </IconButton>\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    children: <PlayIcon />,\n    disabled: true,\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/IconButton/IconButton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './IconButton.module.css'\n\ntype IconButtonProps = {\n  children: React.ReactNode\n} & ComponentProps<'button'>\n\nexport const IconButton = ({ children, className, ...props }: IconButtonProps) => {\n  return (\n    <button type=\"button\" className={clsx(s.button, className)} {...props}>\n      {children}\n    </button>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/IconButton/index.ts",
    "content": "export * from './IconButton'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ImageCropper/ImageCropper.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 600px;\n}\n\n.cropperContainer {\n  position: relative;\n\n  overflow: hidden;\n\n  width: 100%;\n  height: 300px;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n}\n\n.zoomControls {\n  margin-top: 16px;\n  padding: 16px;\n  border-radius: 4px;\n  background-color: var(--color-bg-secondary);\n}\n\n.zoomLabel {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 8px;\n}\n\n.zoomValue {\n  font-weight: 600;\n  color: var(--color-accent);\n}\n\n.zoomSlider {\n  cursor: pointer;\n\n  width: 100%;\n  height: 4px;\n  border-radius: 2px;\n\n  appearance: none;\n  background-color: var(--color-border-base);\n  outline: none;\n\n  transition: opacity 200ms ease;\n}\n\n.zoomSlider:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.zoomSlider::-webkit-slider-thumb {\n  cursor: pointer;\n\n  width: 16px;\n  height: 16px;\n  border-radius: 50%;\n\n  appearance: none;\n  background-color: var(--color-accent);\n\n  transition: all 200ms ease;\n}\n\n.zoomSlider::-webkit-slider-thumb:hover:not(:disabled) {\n  transform: scale(1.1);\n}\n\n.zoomSlider::-moz-range-thumb {\n  cursor: pointer;\n\n  width: 16px;\n  height: 16px;\n  border: none;\n  border-radius: 50%;\n\n  background-color: var(--color-accent);\n\n  transition: all 200ms ease;\n}\n\n.zoomSlider::-moz-range-thumb:hover:not(:disabled) {\n  transform: scale(1.1);\n}\n\n.zoomSlider::-moz-range-track {\n  width: 100%;\n  height: 4px;\n  border-radius: 2px;\n  background-color: var(--color-border-base);\n}\n\n/* Focus states for better accessibility */\n.zoomSlider:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ImageCropper/ImageCropper.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ImageCropper } from './ImageCropper'\n\nconst meta = {\n  title: 'Components/ImageCropper',\n  component: ImageCropper,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    isOpen: false,\n    onClose: () => {},\n    onCropComplete: () => {},\n    imageSrc: '',\n  },\n} satisfies Meta<typeof ImageCropper>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n// Sample image for demonstration\nconst sampleImageSrc = 'https://unsplash.it/600/600'\n\nexport const SquareCrop: Story = {\n  args: {\n    isOpen: true,\n    imageSrc: sampleImageSrc,\n    cropShape: 'rect',\n  },\n}\n\nexport const RoundCrop: Story = {\n  args: {\n    isOpen: true,\n    imageSrc: sampleImageSrc,\n    cropShape: 'round',\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ImageCropper/ImageCropper.tsx",
    "content": "import { clsx } from 'clsx'\nimport { useState } from 'react'\nimport type { Area } from 'react-easy-crop'\nimport Cropper from 'react-easy-crop'\n\nimport { Button } from '../Button'\nimport { Dialog, DialogContent, DialogFooter } from '../Dialog'\nimport { Typography } from '../Typography'\nimport s from './ImageCropper.module.css'\n\nexport type CropShape = 'rect' | 'round'\n\nexport type ImageCropperProps = {\n  isOpen: boolean\n  onClose: () => void\n  onCropComplete: (croppedFile: File, croppedArea: Area) => void\n  imageSrc: string\n  originalFileName?: string\n  cropShape?: CropShape\n  className?: string\n}\n\nexport const ImageCropper = ({\n  isOpen,\n  onClose,\n  onCropComplete,\n  imageSrc,\n  originalFileName = 'cropped-image.jpg',\n  cropShape = 'rect',\n  className,\n}: ImageCropperProps) => {\n  const [crop, setCrop] = useState({\n    x: 0,\n    y: 0,\n  })\n  const [zoom, setZoom] = useState(1)\n  const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null)\n  const [isProcessing, setIsProcessing] = useState(false)\n\n  const onCropAreaChange = (croppedArea: Area, croppedAreaPixels: Area) => {\n    setCroppedAreaPixels(croppedAreaPixels)\n  }\n\n  const getCroppedImg = async (imageSrc: string, pixelCrop: Area): Promise<File> => {\n    const image = new Image()\n    image.src = imageSrc\n\n    return new Promise((resolve, reject) => {\n      image.onload = () => {\n        // Create a canvas element to crop the image\n\n        const canvas = document.createElement('canvas')\n        const ctx = canvas.getContext('2d')\n\n        if (!ctx) {\n          reject(new Error('No 2d context'))\n          return\n        }\n\n        canvas.width = pixelCrop.width\n        canvas.height = pixelCrop.height\n\n        // draw the cropped image on the canvas\n        ctx.drawImage(\n          image,\n          pixelCrop.x,\n          pixelCrop.y,\n          pixelCrop.width,\n          pixelCrop.height,\n          0,\n          0,\n          pixelCrop.width,\n          pixelCrop.height\n        )\n\n        // convert the canvas to a blob\n        canvas.toBlob(\n          (blob) => {\n            if (!blob) {\n              reject(new Error('Canvas is empty'))\n              return\n            }\n\n            // create a file from the blob\n            const file = new File([blob], originalFileName, {\n              type: 'image/jpeg',\n            })\n\n            resolve(file)\n          },\n          'image/jpeg',\n          0.9\n        )\n      }\n\n      image.onerror = () => {\n        reject(new Error('Failed to load image'))\n      }\n    })\n  }\n\n  const handleCropConfirm = async () => {\n    if (!croppedAreaPixels) {\n      return\n    }\n\n    setIsProcessing(true)\n\n    try {\n      const croppedFile = await getCroppedImg(imageSrc, croppedAreaPixels)\n      onCropComplete(croppedFile, croppedAreaPixels)\n      handleReset()\n    } catch (error) {\n      console.error('Error cropping image:', error)\n      // You might want to show an error message to the user here\n    } finally {\n      setIsProcessing(false)\n    }\n  }\n\n  const handleCancel = () => {\n    handleReset()\n    onClose()\n  }\n\n  const handleReset = () => {\n    setCrop({\n      x: 0,\n      y: 0,\n    })\n    setZoom(1)\n    setCroppedAreaPixels(null)\n    setIsProcessing(false)\n  }\n\n  // Reset state when dialog opens/closes\n  const handleClose = () => {\n    if (!isProcessing) {\n      handleCancel()\n    }\n  }\n\n  return (\n    <Dialog open={isOpen} onClose={handleClose} className={clsx(s.dialog, className)}>\n      <DialogContent className={s.dialogContent}>\n        <div className={s.cropperContainer}>\n          {imageSrc && (\n            <Cropper\n              image={imageSrc}\n              crop={crop}\n              zoom={zoom}\n              aspect={1}\n              cropShape={cropShape}\n              onCropChange={setCrop}\n              onCropComplete={onCropAreaChange}\n              onZoomChange={setZoom}\n              showGrid={cropShape === 'rect'}\n            />\n          )}\n        </div>\n\n        <div className={s.zoomControls}>\n          <div className={s.zoomLabel}>\n            <Typography variant=\"body2\">Zoom</Typography>\n            <Typography variant=\"body2\" className={s.zoomValue}>\n              {Math.round(zoom * 100)}%\n            </Typography>\n          </div>\n          <input\n            type=\"range\"\n            value={zoom}\n            min={1}\n            max={3}\n            step={0.05}\n            onChange={(e) => setZoom(Number(e.target.value))}\n            className={s.zoomSlider}\n            disabled={isProcessing}\n          />\n        </div>\n      </DialogContent>\n\n      <DialogFooter>\n        <Button variant=\"secondary\" onClick={handleCancel} disabled={isProcessing}>\n          Cancel\n        </Button>\n        <Button\n          variant=\"primary\"\n          onClick={handleCropConfirm}\n          disabled={!croppedAreaPixels || isProcessing}>\n          {isProcessing ? 'Processing...' : 'Apply Crop'}\n        </Button>\n      </DialogFooter>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ImageCropper/index.ts",
    "content": "export * from './ImageCropper'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ImageUploader/ImageUploader.module.css",
    "content": ".container {\n  width: 100%;\n}\n\n.dropZone {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 100%;\n  min-height: 280px;\n  border: 2px dashed var(--color-border-input-primary);\n  border-radius: 8px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover,\n.dropZone:focus-within {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.dragOver {\n  border-color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.hasPreview {\n  border-color: var(--color-border-input-active);\n  border-style: solid;\n}\n\n.dropZone.error {\n  border-color: var(--color-text-error);\n}\n\n.hiddenInput {\n  position: absolute;\n\n  overflow: hidden;\n\n  width: 1px;\n  height: 1px;\n\n  opacity: 0;\n  clip: rect(0, 0, 0, 0);\n}\n\n.uploadContent {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  align-items: center;\n\n  padding: 32px 16px;\n}\n\n.uploadIcon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n\n  color: var(--color-text-secondary);\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover .uploadIcon,\n.dropZone:focus-within .uploadIcon {\n  color: var(--color-accent);\n  background-color: var(--color-bg-card);\n}\n\n.uploadText {\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  transition: color 200ms ease;\n}\n\n.dropZone:hover .uploadText {\n  color: var(--color-text-primary);\n}\n\n.previewContainer {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.previewImage {\n  width: 100%;\n  height: 100%;\n  min-height: 200px;\n  border-radius: 6px;\n\n  object-fit: cover;\n}\n\n.removeButton {\n  position: absolute;\n  top: 8px;\n  right: 8px;\n}\n\n.removeButton:hover {\n  opacity: 1;\n  background-color: var(--color-text-error);\n}\n\n.removeButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.errorMessage {\n  margin-top: 8px;\n}\n\n/* States for different sizes */\n.dropZone.small {\n  min-height: 120px;\n}\n\n.dropZone.large {\n  min-height: 300px;\n}\n\n/* Responsive adjustments */\n@media (width <= 640px) {\n  .cropperContainer {\n    height: 300px;\n  }\n\n  .uploadContent {\n    padding: 24px 12px;\n  }\n\n  .uploadIcon {\n    width: 40px;\n    height: 40px;\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ImageUploader/ImageUploader.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\nimport type { Area } from 'react-easy-crop'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { ImageUploader } from './ImageUploader'\n\nconst meta = {\n  title: 'Components/ImageUploader',\n  component: ImageUploader,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    onImageSelect: () => {},\n  },\n} satisfies Meta<typeof ImageUploader>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    placeholder: 'Upload Cover Image',\n    enableCrop: true,\n    cropShape: 'rect',\n    aspectRatio: 1,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const WithoutCropping: Story = {\n  args: {\n    placeholder: 'Upload Image (No Crop)',\n    enableCrop: false,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const RoundCrop: Story = {\n  args: {\n    placeholder: 'Upload Avatar Image',\n    enableCrop: true,\n    cropShape: 'round',\n    aspectRatio: 1,\n    cropTitle: 'Crop Your Avatar',\n    cropDescription: 'Position your face in the center of the circle',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const BannerCrop: Story = {\n  args: {\n    placeholder: 'Upload Banner Image',\n    enableCrop: true,\n    cropShape: 'rect',\n    aspectRatio: 16 / 9,\n    cropTitle: 'Create Banner',\n    cropDescription: 'Crop your image to fit banner dimensions (16:9)',\n  },\n  render: (args) => (\n    <div style={{ width: '400px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const PortraitCrop: Story = {\n  args: {\n    placeholder: 'Upload Portrait Image',\n    enableCrop: true,\n    cropShape: 'rect',\n    aspectRatio: 3 / 4,\n    cropTitle: 'Crop Portrait',\n    cropDescription: 'Create a portrait crop with 3:4 aspect ratio',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const CustomFileRestrictions: Story = {\n  args: {\n    placeholder: 'Upload JPEG/PNG only',\n    acceptedFormats: ['image/jpeg', 'image/png'],\n    enableCrop: true,\n    cropShape: 'rect',\n    aspectRatio: 1,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n      <Typography\n        variant=\"caption\"\n        style={{\n          marginTop: '8px',\n          display: 'block',\n        }}>\n        Accepts only JPEG/PNG files up to 2MB\n      </Typography>\n    </div>\n  ),\n}\n\nexport const Interactive = {\n  render: () => {\n    const [uploadedImages, setUploadedImages] = useState<\n      Array<{\n        file: File\n        croppedArea?: Area\n        type: string\n        url: string\n      }>\n    >([])\n\n    const handleImageSelect = (type: string) => (file: File, croppedArea?: Area) => {\n      const url = URL.createObjectURL(file)\n      setUploadedImages((prev) => [\n        ...prev,\n        {\n          file,\n          croppedArea,\n          type,\n          url,\n        },\n      ])\n    }\n\n    const clearResults = () => {\n      // Cleanup URLs to prevent memory leaks\n      uploadedImages.forEach((img) => URL.revokeObjectURL(img.url))\n      setUploadedImages([])\n    }\n\n    const uploaderConfigs = [\n      {\n        id: 'square',\n        title: 'Square Crop',\n        placeholder: 'Upload Square Image',\n        cropShape: 'rect' as const,\n        aspectRatio: 1,\n        cropTitle: 'Square Crop',\n        width: '250px',\n      },\n      {\n        id: 'round',\n        title: 'Round Avatar',\n        placeholder: 'Upload Avatar',\n        cropShape: 'round' as const,\n        aspectRatio: 1,\n        cropTitle: 'Avatar Crop',\n        width: '250px',\n      },\n      {\n        id: 'banner',\n        title: 'Banner Crop',\n        placeholder: 'Upload Banner',\n        cropShape: 'rect' as const,\n        aspectRatio: 16 / 9,\n        cropTitle: 'Banner Crop',\n        width: '350px',\n      },\n      {\n        id: 'no-crop',\n        title: 'No Cropping',\n        placeholder: 'Upload Original',\n        enableCrop: false,\n        width: '250px',\n      },\n    ]\n\n    return (\n      <div\n        style={{\n          width: '100%',\n          maxWidth: '800px',\n        }}>\n        <Typography\n          variant=\"h2\"\n          style={{\n            marginBottom: '24px',\n            textAlign: 'center',\n          }}>\n          Interactive Image Uploader\n        </Typography>\n\n        <Card\n          style={{\n            padding: '20px',\n            marginBottom: '20px',\n          }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Try Different Upload Types\n          </Typography>\n\n          <div\n            style={{\n              display: 'grid',\n              gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',\n              gap: '16px',\n              marginBottom: '20px',\n            }}>\n            {uploaderConfigs.map((config) => (\n              <div key={config.id} style={{ width: config.width }}>\n                <Typography\n                  variant=\"body2\"\n                  style={{\n                    marginBottom: '8px',\n                    fontWeight: '600',\n                  }}>\n                  {config.title}\n                </Typography>\n                <ImageUploader\n                  placeholder={config.placeholder}\n                  enableCrop={config.enableCrop ?? true}\n                  cropShape={config.cropShape}\n                  aspectRatio={config.aspectRatio}\n                  cropTitle={config.cropTitle}\n                  onImageSelect={handleImageSelect(config.title)}\n                />\n              </div>\n            ))}\n          </div>\n        </Card>\n\n        {uploadedImages.length > 0 && (\n          <Card style={{ padding: '20px' }}>\n            <div\n              style={{\n                display: 'flex',\n                justifyContent: 'space-between',\n                alignItems: 'center',\n                marginBottom: '16px',\n              }}>\n              <Typography variant=\"h3\">Uploaded Images ({uploadedImages.length})</Typography>\n              <button\n                onClick={clearResults}\n                style={{\n                  padding: '6px 12px',\n                  backgroundColor: 'var(--color-bg-secondary)',\n                  border: '1px solid var(--color-border-base)',\n                  borderRadius: '4px',\n                  cursor: 'pointer',\n                }}>\n                Clear All\n              </button>\n            </div>\n\n            <div\n              style={{\n                display: 'grid',\n                gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',\n                gap: '16px',\n              }}>\n              {uploadedImages.map((image, index) => (\n                <div\n                  key={index}\n                  style={{\n                    border: '1px solid var(--color-border-base)',\n                    borderRadius: '8px',\n                    padding: '12px',\n                    backgroundColor: 'var(--color-bg-secondary)',\n                  }}>\n                  <img\n                    src={image.url}\n                    alt={`Uploaded ${image.type}`}\n                    style={{\n                      width: '100%',\n                      height: '120px',\n                      objectFit: 'cover',\n                      borderRadius: '4px',\n                      marginBottom: '8px',\n                    }}\n                  />\n                  <Typography\n                    variant=\"body3\"\n                    style={{\n                      fontWeight: '600',\n                      marginBottom: '4px',\n                    }}>\n                    {image.type}\n                  </Typography>\n                  <Typography variant=\"caption\">\n                    {Math.round(image.file.size / 1024)} KB\n                    {image.croppedArea && (\n                      <span style={{ display: 'block' }}>\n                        {Math.round(image.croppedArea.width)}×{Math.round(image.croppedArea.height)}\n                        px\n                      </span>\n                    )}\n                  </Typography>\n                </div>\n              ))}\n            </div>\n          </Card>\n        )}\n\n        <div\n          style={{\n            marginTop: '20px',\n            textAlign: 'center',\n          }}>\n          <Typography variant=\"caption\">\n            Upload images using different cropping modes to see the results\n          </Typography>\n        </div>\n      </div>\n    )\n  },\n}\n\nexport const AllVariants = {\n  render: () => {\n    const [results, setResults] = useState<Record<string, { file: File; url: string }>>({})\n\n    const handleImageSelect = (variant: string) => (file: File) => {\n      const url = URL.createObjectURL(file)\n      setResults((prev) => ({\n        ...prev,\n        [variant]: {\n          file,\n          url,\n        },\n      }))\n    }\n\n    const variants = [\n      {\n        key: 'square',\n        title: 'Square (1:1)',\n        cropShape: 'rect' as const,\n        aspectRatio: 1,\n      },\n      {\n        key: 'round',\n        title: 'Round Avatar',\n        cropShape: 'round' as const,\n        aspectRatio: 1,\n      },\n      {\n        key: 'banner',\n        title: 'Banner (16:9)',\n        cropShape: 'rect' as const,\n        aspectRatio: 16 / 9,\n      },\n      {\n        key: 'portrait',\n        title: 'Portrait (3:4)',\n        cropShape: 'rect' as const,\n        aspectRatio: 3 / 4,\n      },\n      {\n        key: 'landscape',\n        title: 'Landscape (4:3)',\n        cropShape: 'rect' as const,\n        aspectRatio: 4 / 3,\n      },\n      {\n        key: 'no-crop',\n        title: 'No Cropping',\n        enableCrop: false,\n      },\n    ]\n\n    return (\n      <div\n        style={{\n          width: '100%',\n          maxWidth: '1000px',\n        }}>\n        <Typography\n          variant=\"h2\"\n          style={{\n            marginBottom: '24px',\n            textAlign: 'center',\n          }}>\n          All ImageUploader Variants\n        </Typography>\n\n        <div\n          style={{\n            display: 'grid',\n            gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',\n            gap: '20px',\n          }}>\n          {variants.map((variant) => (\n            <Card key={variant.key} style={{ padding: '16px' }}>\n              <Typography\n                variant=\"h3\"\n                style={{\n                  marginBottom: '12px',\n                  textAlign: 'center',\n                }}>\n                {variant.title}\n              </Typography>\n\n              <ImageUploader\n                placeholder={`Upload ${variant.title}`}\n                enableCrop={variant.enableCrop ?? true}\n                cropShape={variant.cropShape}\n                aspectRatio={variant.aspectRatio}\n                onImageSelect={handleImageSelect(variant.key)}\n              />\n\n              {results[variant.key] && (\n                <div\n                  style={{\n                    marginTop: '12px',\n                    textAlign: 'center',\n                  }}>\n                  <img\n                    src={results[variant.key].url}\n                    alt={`Result for ${variant.title}`}\n                    style={{\n                      width: '100%',\n                      maxHeight: '120px',\n                      objectFit: 'cover',\n                      borderRadius: '4px',\n                      marginBottom: '8px',\n                    }}\n                  />\n                  <Typography variant=\"caption\">\n                    {Math.round(results[variant.key].file.size / 1024)} KB\n                  </Typography>\n                </div>\n              )}\n            </Card>\n          ))}\n        </div>\n\n        <div\n          style={{\n            marginTop: '24px',\n            textAlign: 'center',\n          }}>\n          <Typography variant=\"caption\">\n            Upload images to see how different crop settings affect the result\n          </Typography>\n        </div>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ImageUploader/ImageUploader.tsx",
    "content": "import { clsx } from 'clsx'\nimport { t } from 'i18next'\nimport { type ChangeEvent, type DragEvent, useRef, useState } from 'react'\n\nimport { ImageUploadIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { type CropShape, ImageCropper } from '../ImageCropper'\nimport { Typography } from '../Typography'\nimport s from './ImageUploader.module.css'\n\nexport type ImageUploaderProps = {\n  onImageSelect: (file: File) => void\n  className?: string\n  acceptedFormats?: string[]\n  placeholder?: string\n  cropShape?: CropShape\n  aspectRatio?: number\n  enableCrop?: boolean\n  cropTitle?: string\n  cropDescription?: string\n  initialImageUrl?: string\n}\n\nconst MAX_SIZE_IN_MB = 5\nconst ACCEPTED_FORMATS = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']\n\nexport const ImageUploader = ({\n  className,\n  onImageSelect,\n  placeholder = t('placeholder.upload_cover_image'),\n  cropShape = 'rect',\n  enableCrop = true,\n  initialImageUrl,\n}: ImageUploaderProps) => {\n  const [isDragOver, setIsDragOver] = useState(false)\n  const [preview, setPreview] = useState<string | null>(initialImageUrl || null)\n  const [originalFile, setOriginalFile] = useState<File | null>(null)\n  const [error, setError] = useState<string | null>(null)\n  const [showCropModal, setShowCropModal] = useState(false)\n  const fileInputRef = useRef<HTMLInputElement>(null)\n\n  const validateFile = (file: File): string | null => {\n    if (!ACCEPTED_FORMATS.includes(file.type)) {\n      return `Only ${ACCEPTED_FORMATS.join(', ')} files are allowed`\n    }\n\n    const maxSizeInBytes = MAX_SIZE_IN_MB * 1024 * 1024\n    if (file.size > maxSizeInBytes) {\n      return `File size must be less than ${MAX_SIZE_IN_MB}MB`\n    }\n\n    return null\n  }\n\n  const handleFileSelect = (file: File) => {\n    const validationError = validateFile(file)\n\n    if (validationError) {\n      setError(validationError)\n      setPreview(null)\n      return\n    }\n\n    setError(null)\n    setOriginalFile(file)\n\n    // Create preview\n    const reader = new FileReader()\n    reader.onload = (e) => {\n      const imageUrl = e.target?.result as string\n      setPreview(imageUrl)\n\n      if (enableCrop) {\n        setShowCropModal(true)\n      } else {\n        onImageSelect(file)\n      }\n    }\n    reader.readAsDataURL(file)\n  }\n\n  const handleCropComplete = (croppedFile: File) => {\n    // Create preview for cropped image\n    const reader = new FileReader()\n    reader.onload = (e) => {\n      setPreview(e.target?.result as string)\n    }\n    reader.readAsDataURL(croppedFile)\n\n    setShowCropModal(false)\n    onImageSelect(croppedFile)\n  }\n\n  const handleCropCancel = () => {\n    setShowCropModal(false)\n    setPreview(null)\n    setOriginalFile(null)\n  }\n\n  const handleFileInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0]\n    if (file) {\n      handleFileSelect(file)\n    }\n  }\n\n  const handleDragOver = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(true)\n  }\n\n  const handleDragLeave = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n  }\n\n  const handleDrop = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n\n    const files = Array.from(e.dataTransfer.files)\n    const imageFile = files.find((file) => file.type.startsWith('image/'))\n\n    if (imageFile) {\n      handleFileSelect(imageFile)\n    }\n  }\n\n  const handleRemoveImage = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    setPreview(null)\n    setOriginalFile(null)\n    setError(null)\n    // Clear input value to allow selecting the same file again\n    if (fileInputRef.current) {\n      fileInputRef.current.value = ''\n    }\n  }\n\n  return (\n    <>\n      <div className={clsx(s.container, className)}>\n        <label\n          className={clsx(\n            s.dropZone,\n            isDragOver && s.dragOver,\n            preview && s.hasPreview,\n            error && s.error\n          )}\n          onDragOver={handleDragOver}\n          onDragLeave={handleDragLeave}\n          onDrop={handleDrop}>\n          <input\n            ref={fileInputRef}\n            type=\"file\"\n            accept={ACCEPTED_FORMATS.join(',')}\n            onChange={handleFileInputChange}\n            className={s.hiddenInput}\n            tabIndex={0}\n          />\n\n          {preview ? (\n            <div className={s.previewContainer}>\n              <img src={preview} alt=\"Preview\" className={s.previewImage} />\n              <IconButton\n                className={s.removeButton}\n                onClick={handleRemoveImage}\n                aria-label=\"Remove image\"\n                type=\"button\">\n                ✕\n              </IconButton>\n            </div>\n          ) : (\n            <div className={s.uploadContent}>\n              <div className={s.uploadIcon}>\n                <ImageUploadIcon width={24} height={24} />\n              </div>\n              <Typography variant=\"body2\" className={s.uploadText}>\n                {placeholder}\n              </Typography>\n            </div>\n          )}\n        </label>\n\n        {error && (\n          <Typography variant=\"error\" className={s.errorMessage}>\n            {error}\n          </Typography>\n        )}\n      </div>\n\n      {/* Use the standalone ImageCropper component */}\n      {enableCrop && preview && originalFile && (\n        <ImageCropper\n          isOpen={showCropModal}\n          onClose={handleCropCancel}\n          onCropComplete={handleCropComplete}\n          imageSrc={preview}\n          originalFileName={originalFile.name}\n          cropShape={cropShape}\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ImageUploader/index.ts",
    "content": "export * from './ImageUploader'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Loader/Loader.module.css",
    "content": ".root {\n  position: fixed;\n  z-index: 11;\n  top: 0;\n  right: 0;\n  left: 0;\n\n  width: 100%;\n  height: 0.25em;\n  border: none;\n\n  font-size: 16px;\n  color: var(--color-accent);\n\n  appearance: none;\n  background-color: transparent;\n}\n\n.root::-webkit-progress-bar {\n  background-color: transparent;\n}\n\n/* Determinate */\n.root::-webkit-progress-value {\n  background-color: currentcolor;\n  transition: all 0.2s;\n}\n\n.root::-moz-progress-bar {\n  background-color: currentcolor;\n  transition: all 0.2s;\n}\n\n.root::-ms-fill {\n  border: none;\n  background-color: currentcolor;\n  transition: all 0.2s;\n}\n\n/* Indeterminate */\n.root:indeterminate {\n  background-image: linear-gradient(\n    to right,\n    transparent 50%,\n    currentcolor 50%,\n    currentcolor 60%,\n    transparent 60%,\n    transparent 71.5%,\n    currentcolor 71.5%,\n    currentcolor 84%,\n    transparent 84%\n  );\n  background-size: 200% 100%;\n  animation: root 2s infinite linear;\n}\n\n.root:indeterminate::-moz-progress-bar {\n  background-color: transparent;\n}\n\n.root:indeterminate::-ms-fill {\n  animation-name: none;\n}\n\n@keyframes root {\n  0% {\n    background-position: left -31.25% top 0%;\n    background-size: 200% 100%;\n  }\n\n  50% {\n    background-position: left -49% top 0%;\n    background-size: 800% 100%;\n  }\n\n  100% {\n    background-position: left -102% top 0%;\n    background-size: 400% 100%;\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Loader/Loader.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './Loader.module.css'\nexport type LoaderProps = ComponentProps<'progress'>\n\nexport const Loader = ({ className, ...restProps }: LoaderProps) => {\n  return <progress className={clsx(s.root, className)} {...restProps} />\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Loader/index.ts",
    "content": "export * from './Loader'\nexport * from '../Spinner/Spinner.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Pagination/Pagination.module.css",
    "content": ".pagination {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n}\n\n.navButton {\n  width: 40px;\n  height: 40px;\n  border-radius: 4px;\n\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.navButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n  background-color: var(--color-bg-secondary);\n}\n\n.navButton:enabled:hover,\n.navButton:enabled:focus {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageNumbers {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n}\n\n.pageButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n  border: none;\n  border-radius: 8px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.pageButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.pageButton:hover:not(.active) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageButton.active {\n  background-color: var(--color-accent);\n}\n\n.pageButton.active:hover {\n  opacity: 0.9;\n}\n\n.ellipsis {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* Responsive adjustments */\n@media (width <= 480px) {\n  .pagination {\n    gap: 2px;\n  }\n\n  .navButton,\n  .pageButton,\n  .ellipsis {\n    width: 36px;\n    height: 36px;\n  }\n\n  .pageButton,\n  .ellipsis {\n    font-size: var(--font-size-s);\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Pagination/Pagination.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Pagination } from './Pagination'\n\nconst meta = {\n  title: 'Components/Pagination',\n  component: Pagination,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Pagination>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    page: 1,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const MiddlePage: Story = {\n  args: {\n    page: 5,\n    pagesCount: 10,\n    onPageChange: () => {},\n  },\n}\n\nexport const LastPage: Story = {\n  args: {\n    page: 3,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const ManyPages: Story = {\n  args: {\n    page: 8,\n    pagesCount: 20,\n    onPageChange: () => {},\n  },\n}\n\nexport const SinglePage: Story = {\n  args: {\n    page: 1,\n    pagesCount: 1,\n    onPageChange: () => {},\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [currentPage, setCurrentPage] = useState(1)\n    const totalCount = 95\n    const pageSize = 10\n    const pagesCount = Math.ceil(totalCount / pageSize)\n\n    const handlePageChange = (page: number) => {\n      setCurrentPage(page)\n    }\n\n    return (\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n          alignItems: 'center',\n          width: '500px',\n        }}>\n        <Card style={{ padding: '20px', textAlign: 'center' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Interactive Pagination\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Current page: <strong>{currentPage}</strong>\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Total items: <strong>{totalCount}</strong>\n          </Typography>\n          <Typography variant=\"body2\">\n            Items per page: <strong>{pageSize}</strong>\n          </Typography>\n        </Card>\n\n        <Pagination page={currentPage} pagesCount={pagesCount} onPageChange={handlePageChange} />\n\n        <Typography variant=\"caption\" style={{ textAlign: 'center' }}>\n          Click on page numbers or arrows to navigate\n        </Typography>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '32px',\n        alignItems: 'center',\n        width: '600px',\n      }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          First Page (3 pages total)\n        </Typography>\n        <Pagination page={1} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Middle Page (10 pages total)\n        </Typography>\n        <Pagination page={5} pagesCount={10} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Last Page (3 pages total)\n        </Typography>\n        <Pagination page={3} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Many Pages (20 pages total)\n        </Typography>\n        <Pagination page={12} pagesCount={20} onPageChange={() => {}} />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Pagination/Pagination.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport { KeyboardArrowLeftIcon, KeyboardArrowRightIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './Pagination.module.css'\n\nexport type PaginationProps = {\n  page: number\n  pagesCount: number\n  onPageChange: (page: number) => void\n  alwaysVisible?: boolean\n  className?: string\n} & Omit<ComponentProps<'div'>, 'children'>\n\nconst MAX_VISIBLE_PAGES = 5\n\nexport const Pagination = ({\n  page,\n\n  pagesCount,\n  onPageChange,\n  alwaysVisible = false,\n  className,\n  ...props\n}: PaginationProps) => {\n  const normalizedPagesCount = Math.max(1, pagesCount)\n  const normalizedPage = Math.min(Math.max(1, page), normalizedPagesCount)\n\n  if (!alwaysVisible && normalizedPagesCount <= 1) {\n    return null\n  }\n\n  // Helper function to generate page numbers array\n  const generatePageNumbers = () => {\n    const pages: (number | 'ellipsis')[] = []\n\n    if (normalizedPagesCount <= MAX_VISIBLE_PAGES) {\n      // Show all pages if total is small\n      for (let i = 1; i <= normalizedPagesCount; i++) {\n        pages.push(i)\n      }\n    } else {\n      // Always show first page\n      pages.push(1)\n\n      if (normalizedPage > 3) {\n        pages.push('ellipsis')\n      }\n\n      // Show pages around current page\n      const start = Math.max(2, normalizedPage - 1)\n      const end = Math.min(normalizedPagesCount - 1, normalizedPage + 1)\n\n      for (let i = start; i <= end; i++) {\n        if (i !== 1 && i !== normalizedPagesCount) {\n          pages.push(i)\n        }\n      }\n\n      if (normalizedPage < normalizedPagesCount - 2) {\n        pages.push('ellipsis')\n      }\n\n      // Always show last page if it's not already included\n      if (normalizedPagesCount > 1) {\n        pages.push(normalizedPagesCount)\n      }\n    }\n\n    return pages\n  }\n\n  const handlePrevious = () => {\n    if (normalizedPage > 1) {\n      onPageChange(normalizedPage - 1)\n    }\n  }\n\n  const handleNext = () => {\n    if (normalizedPage < normalizedPagesCount) {\n      onPageChange(normalizedPage + 1)\n    }\n  }\n\n  const handlePageClick = (pageNumber: number) => {\n    onPageChange(pageNumber)\n  }\n\n  const pageNumbers = generatePageNumbers()\n\n  return (\n    <div\n      className={clsx(s.pagination, className)}\n      role=\"navigation\"\n      aria-label=\"Pagination\"\n      {...props}>\n      {/* Previous button */}\n      <IconButton\n        onClick={handlePrevious}\n        disabled={normalizedPage === 1}\n        aria-label=\"Go to previous page\"\n        className={s.navButton}>\n        <KeyboardArrowLeftIcon />\n      </IconButton>\n\n      {/* Page numbers */}\n      <div className={s.pageNumbers}>\n        {pageNumbers.map((pageNumber, index) => {\n          if (pageNumber === 'ellipsis') {\n            return (\n              <span key={`ellipsis-${index}`} className={s.ellipsis} aria-hidden=\"true\">\n                ...\n              </span>\n            )\n          }\n\n          const isActive = pageNumber === normalizedPage\n\n          return (\n            <button\n              key={pageNumber}\n              onClick={() => handlePageClick(pageNumber)}\n              className={clsx(s.pageButton, isActive && s.active)}\n              aria-label={`Go to page ${pageNumber}`}\n              aria-current={isActive ? 'page' : undefined}\n              type=\"button\">\n              {pageNumber}\n            </button>\n          )\n        })}\n      </div>\n\n      {/* Next button */}\n      <IconButton\n        onClick={handleNext}\n        disabled={normalizedPage === normalizedPagesCount}\n        aria-label=\"Go to next page\"\n        className={s.navButton}>\n        <KeyboardArrowRightIcon />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Pagination/index.ts",
    "content": "export * from './Pagination'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Progress/Progress.module.css",
    "content": ".progress {\n  overflow: hidden;\n\n  width: 100%;\n  height: 4px;\n  border-radius: 4px;\n\n  background-color: var(--color-border-base);\n}\n\n.progressBar {\n  height: 100%;\n  border-radius: 4px;\n  background-color: var(--color-accent);\n  transition: width 300ms ease;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Progress/Progress.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Progress } from './Progress'\n\nconst meta = {\n  title: 'Components/Progress',\n  component: Progress,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Progress>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    value: 75,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const CustomMax: Story = {\n  args: {\n    value: 15,\n    max: 20,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Empty: Story = {\n  args: {\n    value: 0,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Full: Story = {\n  args: {\n    value: 100,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Empty (0%)\n        </Typography>\n        <Progress value={0} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Low (25%)\n        </Typography>\n        <Progress value={25} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Medium (50%)\n        </Typography>\n        <Progress value={50} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          High (85%)\n        </Typography>\n        <Progress value={85} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Complete (100%)\n        </Typography>\n        <Progress value={100} />\n      </div>\n    </div>\n  ),\n}\n\nexport const CustomSizes = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Small (height: 4px)\n        </Typography>\n        <Progress value={70} style={{ height: '4px' }} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Default (height: 8px)\n        </Typography>\n        <Progress value={70} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Large (height: 12px)\n        </Typography>\n        <Progress value={70} style={{ height: '12px' }} />\n      </div>\n    </div>\n  ),\n}\n\nexport const Interactive = {\n  render: () => {\n    const [progress, setProgress] = useState(0)\n\n    const handleIncrease = () => {\n      setProgress((prev) => Math.min(prev + 10, 100))\n    }\n\n    const handleDecrease = () => {\n      setProgress((prev) => Math.max(prev - 10, 0))\n    }\n\n    const handleReset = () => {\n      setProgress(0)\n    }\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Card style={{ padding: '24px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Interactive Progress\n          </Typography>\n\n          <div style={{ marginBottom: '16px' }}>\n            <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n              Current progress: {progress}%\n            </Typography>\n            <Progress value={progress} />\n          </div>\n\n          <div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>\n            <Button variant=\"secondary\" onClick={handleDecrease} disabled={progress === 0}>\n              -10%\n            </Button>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset\n            </Button>\n            <Button variant=\"primary\" onClick={handleIncrease} disabled={progress === 100}>\n              +10%\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const FileUploadExample = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Card style={{ padding: '24px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          File Upload Progress\n        </Typography>\n\n        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">image.jpg</Typography>\n              <Typography variant=\"body2\">75%</Typography>\n            </div>\n            <Progress value={75} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">document.pdf</Typography>\n              <Typography variant=\"body2\">100%</Typography>\n            </div>\n            <Progress value={100} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">video.mp4</Typography>\n              <Typography variant=\"body2\">32%</Typography>\n            </div>\n            <Progress value={32} />\n          </div>\n        </div>\n      </Card>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Progress/Progress.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './Progress.module.css'\n\nexport type ProgressProps = {\n  value: number\n  max?: number\n} & ComponentProps<'div'>\n\nexport const Progress = ({ value, max = 100, className, ...props }: ProgressProps) => {\n  const percentage = Math.min(Math.max((value / max) * 100, 0), 100)\n\n  return (\n    <div\n      className={clsx(s.progress, className)}\n      role=\"progressbar\"\n      aria-valuenow={value}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      {...props}>\n      <div className={s.progressBar} style={{ width: `${percentage}%` }} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Progress/index.ts",
    "content": "export * from './Progress'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ReactionButtons/ReactionButtons.module.css",
    "content": ".container {\n  display: flex;\n  gap: 8px;\n  align-items: start;\n  width: fit-content;\n}\n\n.button {\n  width: 28px;\n  height: 28px;\n  padding: 0;\n  transition: color 200ms ease;\n}\n\n.button.large {\n  width: 40px;\n  height: 40px;\n}\n\n.button.liked {\n  color: var(--color-accent);\n}\n\n.button.disliked {\n  color: var(--color-accent);\n}\n\n.button:enabled:hover:is(.liked, .disliked),\n.button:enabled:focus:is(.liked, .disliked) {\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.likesCountBox {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.likesCount {\n  font-size: 10px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ReactionButtons/ReactionButtons.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { CurrentUserReaction, ReactionButtons } from './ReactionButtons'\n\nconst meta = {\n  title: 'Components/ReactionButtons',\n  component: ReactionButtons,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof ReactionButtons>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    reaction: CurrentUserReaction.None,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n    onUnReaction: () => console.log('Unreaction!'),\n  },\n}\n\nexport const WithLikesCount: Story = {\n  args: {\n    reaction: CurrentUserReaction.None,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n    onUnReaction: () => console.log('Unreaction!'),\n    likesCount: 10,\n  },\n}\n\nexport const LikedState: Story = {\n  args: {\n    reaction: CurrentUserReaction.Like,\n    onLike: () => console.log('Unlike'),\n    onDislike: () => console.log('Disliked!'),\n    onUnReaction: () => console.log('Unreaction!'),\n  },\n}\n\nexport const DislikedState: Story = {\n  args: {\n    reaction: CurrentUserReaction.Dislike,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Remove dislike'),\n    onUnReaction: () => console.log('Unreaction!'),\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [reaction, setReaction] = useState<CurrentUserReaction>(CurrentUserReaction.None)\n\n    const handleLike = () => {\n      setReaction(\n        reaction === CurrentUserReaction.Like ? CurrentUserReaction.None : CurrentUserReaction.Like\n      )\n    }\n\n    const handleDislike = () => {\n      setReaction(\n        reaction === CurrentUserReaction.Dislike\n          ? CurrentUserReaction.None\n          : CurrentUserReaction.Dislike\n      )\n    }\n\n    return (\n      <Card style={{ padding: '24px', maxWidth: '300px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          Interactive Reaction Buttons\n        </Typography>\n\n        <Typography variant=\"body2\" style={{ marginBottom: '16px' }}>\n          Try clicking the buttons below:\n        </Typography>\n\n        <div style={{ display: 'flex', justifyContent: 'center' }}>\n          <ReactionButtons\n            reaction={reaction}\n            onLike={handleLike}\n            onDislike={handleDislike}\n            onUnReaction={() => console.log('Unreaction!')}\n          />\n        </div>\n\n        <Typography\n          variant=\"caption\"\n          style={{ marginTop: '16px', textAlign: 'center', display: 'block' }}>\n          Status:{' '}\n          {reaction === CurrentUserReaction.Like\n            ? '👍 Liked'\n            : reaction === CurrentUserReaction.Dislike\n              ? '👎 Disliked'\n              : '😐 Neutral'}\n        </Typography>\n      </Card>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Default\n        </Typography>\n        <ReactionButtons\n          reaction={CurrentUserReaction.None}\n          onLike={() => {}}\n          onDislike={() => {}}\n          onUnReaction={() => console.log('Unreaction!')}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Liked\n        </Typography>\n        <ReactionButtons\n          reaction={CurrentUserReaction.Like}\n          onLike={() => {}}\n          onDislike={() => {}}\n          onUnReaction={() => console.log('Unreaction!')}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Disliked\n        </Typography>\n        <ReactionButtons\n          reaction={CurrentUserReaction.Dislike}\n          onLike={() => {}}\n          onUnReaction={() => console.log('Unreaction!')}\n          onDislike={() => {}}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          With likes count\n        </Typography>\n        <ReactionButtons\n          reaction={CurrentUserReaction.None}\n          onUnReaction={() => console.log('Unreaction!')}\n          onLike={() => {}}\n          onDislike={() => {}}\n          likesCount={10}\n        />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ReactionButtons/ReactionButtons.tsx",
    "content": "import { clsx } from 'clsx'\n\nimport { DislikeIcon, LikeIcon, LikeIconFill } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './ReactionButtons.module.css'\n\n// duplication of the CurrentUserReaction type to decouple the shared layer from the features layer\nexport enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = -1,\n}\n\nexport type ReactionButtonsProps = {\n  reaction?: CurrentUserReaction\n  onLike: () => void\n  onDislike: () => void\n  onUnReaction: () => void\n  likesCount?: number\n  className?: string\n  size?: ReactionButtonsSize\n}\n\nconst SIZE_MAP = {\n  small: 28,\n  large: 40,\n}\n\nexport type ReactionButtonsSize = keyof typeof SIZE_MAP\n\nexport const ReactionButtons = ({\n  reaction = CurrentUserReaction.None,\n  onLike,\n  onDislike,\n  onUnReaction,\n  likesCount,\n  className,\n  size = 'small',\n}: ReactionButtonsProps) => {\n  const isLiked = reaction === CurrentUserReaction.Like\n  const isDisliked = reaction === CurrentUserReaction.Dislike\n\n  const iconSize = SIZE_MAP[size]\n\n  return (\n    <div className={clsx(s.container, className)} onClick={(e) => e.preventDefault()}>\n      <div className={s.likesCountBox}>\n        <IconButton\n          onClick={(e) => {\n            e.preventDefault()\n            if (isLiked) {\n              onUnReaction()\n            } else {\n              onLike()\n            }\n          }}\n          className={clsx(s.button, isLiked && s.liked, size === 'large' && s.large)}\n          aria-label={isLiked ? 'Remove like' : 'Like'}\n          type=\"button\">\n          {isLiked ? (\n            <LikeIconFill width={iconSize} height={iconSize} />\n          ) : (\n            <LikeIcon width={iconSize} height={iconSize} />\n          )}\n        </IconButton>\n        <span className={s.likesCount}>{likesCount}</span>\n      </div>\n\n      <IconButton\n        onClick={(e) => {\n          e.preventDefault()\n          if (isDisliked) {\n            onUnReaction()\n          } else {\n            onDislike()\n          }\n        }}\n        className={clsx(s.button, isDisliked && s.disliked, size === 'large' && s.large)}\n        aria-label={isDisliked ? 'Remove dislike' : 'Dislike'}\n        type=\"button\">\n        <DislikeIcon width={iconSize} height={iconSize} />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/ReactionButtons/index.ts",
    "content": "export * from './ReactionButtons'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/SearchField/SearchField.module.css",
    "content": ".inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.searchIcon {\n  pointer-events: none;\n\n  position: absolute;\n  z-index: 1;\n  left: 12px;\n\n  color: var(--color-text-secondary);\n\n  transition: color 200ms ease;\n}\n\n.input {\n  width: 100%;\n  height: 52px;\n  padding: 15px 16px 15px 62px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 26px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary-reverse);\n\n  background-color: var(--color-bg-primary-reverse);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input::placeholder {\n  font-size: var(--font-size-m);\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/SearchField/SearchField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchField } from './SearchField'\n\nconst meta = {\n  title: 'Components/SearchField',\n  component: SearchField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof SearchField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    placeholder: 'Search for playlists...',\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/SearchField/SearchField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport s from './SearchField.module.css'\n\nexport type SearchFieldProps = {\n  label?: ReactNode\n  placeholder?: string\n} & ComponentProps<'input'>\n\nexport const SearchField = ({\n  className,\n  placeholder = 'Search...',\n  ...props\n}: SearchFieldProps) => {\n  return (\n    <div className={clsx(s.inputWrapper, className)}>\n      <SearchIcon className={s.searchIcon} />\n      <input className={clsx(s.input)} type=\"text\" placeholder={placeholder} {...props} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/SearchField/index.ts",
    "content": "export * from './SearchField'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Select/Select.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.selectWrapper {\n  position: relative;\n  width: 100%;\n}\n\n.select {\n  width: 100%;\n  height: 40px;\n  padding: 8px 36px 8px 12px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-decoration: underline;\n  text-underline-offset: 3px;\n\n  appearance: none;\n  background-color: transparent;\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color;\n}\n\n.select:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.select:focus-visible {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select.error {\n  border-color: var(--color-text-error);\n}\n\n/* Style dropdown options */\n.select option {\n  padding: 8px 12px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: background-color 200ms ease;\n}\n\n.select option:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:checked {\n  font-weight: 600;\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:disabled {\n  color: var(--color-disabled);\n}\n\n/* Custom dropdown icon */\n.icon {\n  pointer-events: none;\n\n  position: absolute;\n  top: 50%;\n  right: 12px;\n  transform: translateY(-50%);\n\n  width: 20px;\n  height: 20px;\n\n  color: var(--color-text-secondary);\n\n  transition:\n    color 200ms ease,\n    transform 200ms ease;\n}\n\n/* Rotate icon when dropdown is open */\n.select:open + .icon {\n  transform: translateY(-50%) rotate(180deg);\n}\n\n.label.error {\n  color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Select/Select.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Select } from './Select'\n\nconst meta = {\n  title: 'Components/Select',\n  component: Select,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Select>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst commonOptions = [\n  { value: 'react', label: 'React' },\n  { value: 'vue', label: 'Vue.js' },\n  { value: 'angular', label: 'Angular' },\n  { value: 'svelte', label: 'Svelte' },\n  { value: 'vanilla', label: 'Vanilla JS' },\n]\n\nconst genres = [\n  { value: 'pop', label: 'Pop' },\n  { value: 'rock', label: 'Rock' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hip-hop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n]\n\nexport const AllVariants = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '350px',\n      }}>\n      <Select label=\"Basic Select\" placeholder=\"Choose option\" options={commonOptions} />\n\n      <Select label=\"With Default Value\" options={commonOptions} defaultValue=\"react\" />\n\n      <Select\n        label=\"With Error\"\n        placeholder=\"Choose option\"\n        options={commonOptions}\n        errorMessage=\"This field is required\"\n      />\n\n      <Select label=\"Disabled\" placeholder=\"Cannot select\" options={commonOptions} disabled />\n    </div>\n  ),\n}\n\nexport const Basic: Story = {\n  args: {\n    label: 'Choose framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDefaultValue: Story = {\n  args: {\n    label: 'Preferred framework',\n    options: commonOptions,\n    defaultValue: 'react',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Framework (disabled)',\n    placeholder: 'Cannot select',\n    options: commonOptions,\n    disabled: true,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithError: Story = {\n  args: {\n    label: 'Framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n    errorMessage: 'Please select a framework',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDisabledOptions: Story = {\n  args: {\n    label: 'Music Genre',\n    placeholder: 'Choose your favorite genre',\n    options: [\n      { value: 'pop', label: 'Pop' },\n      { value: 'rock', label: 'Rock' },\n      { value: 'jazz', label: 'Jazz (Coming Soon)', disabled: true },\n      { value: 'classical', label: 'Classical' },\n      { value: 'electronic', label: 'Electronic (Coming Soon)', disabled: true },\n      { value: 'hip-hop', label: 'Hip Hop' },\n    ],\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Controlled = {\n  render: () => {\n    const [value, setValue] = useState('')\n\n    return (\n      <div style={{ width: '400px', display: 'flex', flexDirection: 'column', gap: '16px' }}>\n        <Select\n          label=\"Music Genre\"\n          placeholder=\"Select genre\"\n          options={genres}\n          value={value}\n          onChange={(e) => setValue(e.target.value)}\n        />\n\n        <div\n          style={{\n            padding: '12px',\n            backgroundColor: 'var(--color-bg-card)',\n            borderRadius: '4px',\n            fontSize: 'var(--font-size-s)',\n            color: 'var(--color-text-secondary)',\n          }}>\n          Selected value: <strong>{value || 'None'}</strong>\n        </div>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Select/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          as=\"label\"\n          htmlFor={selectId}\n          className={clsx(s.label, showError && s.error)}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Select/index.ts",
    "content": "export * from './Select'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Skeleton/Skeleton.module.css",
    "content": ".skeleton {\n  position: relative;\n\n  overflow: hidden;\n  display: inline-block;\n\n  width: 100%;\n  height: 16px;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-secondary);\n}\n\n.skeleton::after {\n  content: '';\n\n  position: absolute;\n  inset: 0;\n  transform: translateX(-100%);\n\n  background: linear-gradient(90deg, transparent, rgb(255 255 255 / 10%), transparent);\n\n  animation: shimmer 1.5s infinite;\n}\n\n.circle {\n  width: 40px;\n  height: 40px;\n  border-radius: 50%;\n}\n\n/* Animation */\n@keyframes shimmer {\n  100% {\n    transform: translateX(100%);\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Skeleton/Skeleton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Skeleton } from './Skeleton.tsx'\n\nconst meta = {\n  title: 'Components/Skeleton',\n  component: Skeleton,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Skeleton>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    width: 200,\n    height: 16,\n  },\n}\n\nexport const Circle: Story = {\n  args: {\n    circle: true,\n    width: 60,\n    height: 60,\n  },\n}\n\nexport const AllVariants = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', width: '300px' }}>\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Обычный скелетон\n        </Typography>\n        <Skeleton width={200} height={16} />\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Круглый скелетон\n        </Typography>\n        <Skeleton circle width={50} height={50} />\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Широкий блок\n        </Typography>\n        <Skeleton width=\"100%\" height={100} />\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Узкий блок\n        </Typography>\n        <Skeleton width={100} height={20} />\n      </div>\n    </div>\n  ),\n}\n\nexport const TrackExample = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n        Пример загрузки треков\n      </Typography>\n\n      <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>\n        {Array.from({ length: 3 }, (_, i) => (\n          <div key={i} style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>\n            <Skeleton circle width={50} height={50} />\n            <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '8px' }}>\n              <Skeleton width=\"60%\" height={16} />\n              <Skeleton width=\"40%\" height={14} />\n            </div>\n            <Skeleton width={30} height={14} />\n          </div>\n        ))}\n      </div>\n    </div>\n  ),\n}\n\nexport const CardExample = {\n  render: () => (\n    <div style={{ width: '500px' }}>\n      <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n        Пример карточек\n      </Typography>\n\n      <div\n        style={{\n          display: 'grid',\n          gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',\n          gap: '16px',\n        }}>\n        {Array.from({ length: 4 }, (_, i) => (\n          <Card key={i} style={{ padding: '16px' }}>\n            <Skeleton width=\"100%\" height={100} style={{ marginBottom: '12px' }} />\n            <Skeleton width=\"80%\" height={16} style={{ marginBottom: '8px' }} />\n            <Skeleton width=\"60%\" height={14} />\n          </Card>\n        ))}\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Skeleton/Skeleton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, CSSProperties } from 'react'\n\nimport s from './Skeleton.module.css'\n\nexport type SkeletonProps = {\n  circle?: boolean\n  width?: number | string\n  height?: number | string\n  className?: string\n  style?: CSSProperties\n} & ComponentProps<'div'>\n\nexport const Skeleton = ({\n  circle = false,\n  width,\n  height,\n  className,\n  style,\n  ...props\n}: SkeletonProps) => {\n  return (\n    <div\n      className={clsx(s.skeleton, circle && s.circle, className)}\n      style={{ width, height, ...style }}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Skeleton/index.ts",
    "content": "export * from './Skeleton.tsx'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/SortSelect/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={selectId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Spinner/Spinner.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Spinner } from './Spinner.tsx'\n\nconst meta = {\n  argTypes: {\n    size: {\n      control: { type: 'number' },\n    },\n    fullScreen: {\n      control: { type: 'boolean' },\n    },\n  },\n  component: Spinner,\n  tags: ['autodocs'],\n  title: 'Components/Spinner',\n} satisfies Meta<typeof Spinner>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    fullScreen: false,\n    size: 48,\n  },\n}\n\nexport const FullScreen: Story = {\n  args: {\n    fullScreen: true,\n    size: 100,\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Spinner/Spinner.tsx",
    "content": "import type { FC } from 'react'\n\nimport s from './spinner.module.css'\n\nexport type SpinnerProps = {\n  fullScreen?: boolean\n  size?: number\n}\n\nexport const Spinner: FC<SpinnerProps> = ({ fullScreen = false, size = 48 }) => {\n  const containerStyle = {\n    height: fullScreen ? '100vh' : '100%',\n  }\n\n  const style = {\n    height: size,\n    width: size,\n  }\n\n  return (\n    <div className={s.container} style={containerStyle}>\n      <span className={s.loader} style={style} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Spinner/spinner.module.css",
    "content": ".container {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 100%;\n  height: 100%;\n}\n\n.loader {\n  display: inline-block;\n\n  box-sizing: border-box;\n\n  border-top: 3px solid var(--color-accent);\n  border-right: 3px solid transparent;\n  border-radius: 50%;\n\n  animation: rotation 1s linear infinite;\n}\n\n@keyframes rotation {\n  0% {\n    transform: rotate(0deg);\n  }\n\n  100% {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Table/Table.module.css",
    "content": ".table {\n  table-layout: fixed;\n  border-collapse: collapse;\n  width: 100%;\n  background: transparent;\n}\n\n.tableHead {\n  border-bottom: 1px solid var(--color-border-base);\n}\n\n.tableHeaderCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-xs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  text-align: left;\n  text-transform: uppercase;\n\n  background: transparent;\n}\n\n.tableHeaderCell:first-child {\n  padding-left: 16px;\n}\n\n.tableHeaderCell:last-child {\n  padding-right: 16px;\n}\n\n.tableBody {\n  background: transparent;\n}\n\n.tableRow {\n  transition: background-color 200ms ease;\n}\n\n.tableBody .tableRow:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tableCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  vertical-align: middle;\n\n  background: transparent;\n}\n\n.tableCell:first-child {\n  padding-left: 16px;\n}\n\n.tableCell:last-child {\n  padding-right: 16px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Table/Table.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ReactionButtons } from '../ReactionButtons'\nimport { Typography } from '../Typography'\nimport { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './Table'\nimport s from './Table.module.css'\n\nconst meta = {\n  title: 'Components/Table',\n  component: Table,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Table>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst trackData = [\n  {\n    id: 1,\n    title: 'Play It Safe',\n    artist: 'Julia Wolf',\n    image: 'https://picsum.photos/40/40?random=1',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 2,\n    title: 'Ocean Front Apt.',\n    artist: 'ayokay',\n    image: 'https://picsum.photos/40/40?random=2',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 3,\n    title: 'Free Spirit',\n    artist: 'Khalid',\n    image: 'https://picsum.photos/40/40?random=3',\n    dateAdded: '2 day ago',\n    duration: '3:02',\n  },\n  {\n    id: 4,\n    title: 'Remind You',\n    artist: 'FRENSHIP',\n    image: 'https://picsum.photos/40/40?random=4',\n    dateAdded: '3 day ago',\n    duration: '4:25',\n  },\n]\n\nexport const BasicTable = {\n  render: () => (\n    <div style={{ width: '600px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Name</TableHeaderCell>\n            <TableHeaderCell>Email</TableHeaderCell>\n            <TableHeaderCell>Role</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell>John Doe</TableCell>\n            <TableCell>john@example.com</TableCell>\n            <TableCell>Admin</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Jane Smith</TableCell>\n            <TableCell>jane@example.com</TableCell>\n            <TableCell>User</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Bob Johnson</TableCell>\n            <TableCell>bob@example.com</TableCell>\n            <TableCell>Editor</TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n\nexport const EmptyTable = {\n  render: () => (\n    <div style={{ width: '500px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Column&nbsp;1</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;2</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;3</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell colSpan={3}>\n              <Typography variant=\"body2\" style={{ textAlign: 'center', padding: '40px 20px' }}>\n                No data available\n              </Typography>\n            </TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Table/Table.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport s from './Table.module.css'\n\n/*\n * Table\n */\n\nexport type TableProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'table'>\n\nexport const Table = ({ children, className, ...props }: TableProps) => {\n  return (\n    <table className={clsx(s.table, className)} {...props}>\n      {children}\n    </table>\n  )\n}\n\n/*\n * TableHead\n */\n\nexport type TableHeadProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'thead'>\n\nexport const TableHead = ({ children, className, ...props }: TableHeadProps) => {\n  return (\n    <thead className={clsx(s.tableHead, className)} {...props}>\n      {children}\n    </thead>\n  )\n}\n\n/*\n * TableBody\n */\n\nexport type TableBodyProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tbody'>\n\nexport const TableBody = ({ children, className, ...props }: TableBodyProps) => {\n  return (\n    <tbody className={clsx(s.tableBody, className)} {...props}>\n      {children}\n    </tbody>\n  )\n}\n\n/*\n * TableRow\n */\n\nexport type TableRowProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tr'>\n\nexport const TableRow = ({ children, className, ...props }: TableRowProps) => {\n  return (\n    <tr className={clsx(s.tableRow, className)} {...props}>\n      {children}\n    </tr>\n  )\n}\n\n/*\n * TableHeaderCell\n */\n\nexport type TableHeaderCellProps = {\n  children?: ReactNode\n  className?: string\n} & ComponentProps<'th'>\n\nexport const TableHeaderCell = ({ children, className, ...props }: TableHeaderCellProps) => {\n  return (\n    <th className={clsx(s.tableHeaderCell, className)} {...props}>\n      {children}\n    </th>\n  )\n}\n\n/*\n * TableCell\n */\n\nexport type TableCellProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'td'>\n\nexport const TableCell = ({ children, className, ...props }: TableCellProps) => {\n  return (\n    <td className={clsx(s.tableCell, className)} {...props}>\n      {children}\n    </td>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Table/index.ts",
    "content": "export * from './Table'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Tabs/Tabs.module.css",
    "content": ".tabsList {\n  display: flex;\n  width: 100%;\n  border-bottom: 1px solid var(--color-text-secondary);\n}\n\n.tabsTrigger {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  flex: 1 1 0;\n  align-items: center;\n  justify-content: center;\n\n  padding: 12px 16px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.tabsTrigger:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.tabsTrigger:not(.active, :disabled):hover {\n  opacity: 0.7;\n}\n\n.tabsTrigger.active {\n  color: var(--color-accent);\n}\n\n.tabsTrigger.active::after {\n  content: '';\n\n  position: absolute;\n  bottom: -1px;\n  left: 0;\n\n  width: 100%;\n  height: 2px;\n\n  background-color: var(--color-accent);\n}\n\n.tabsTrigger.disabled {\n  cursor: default;\n  color: var(--color-disabled);\n}\n\n.tabsContent {\n  padding: 32px 0;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Tabs/Tabs.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs'\n\nconst meta = {\n  title: 'Components/Tabs',\n  component: Tabs,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Tabs>\n\nexport default meta\n\nexport const BasicTabs = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Tabs defaultValue=\"account\">\n        <TabsList>\n          <TabsTrigger value=\"account\">Account</TabsTrigger>\n          <TabsTrigger value=\"password\">Password</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"account\">\n          <Typography variant=\"body1\">Make changes to your account here.</Typography>\n        </TabsContent>\n        <TabsContent value=\"password\">\n          <Typography variant=\"body1\">Change your password here.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n\nexport const ControlledTabs = {\n  render: () => {\n    const [activeTab, setActiveTab] = useState('tab1')\n\n    return (\n      <div style={{ width: '500px' }}>\n        <Tabs value={activeTab} onValueChange={setActiveTab}>\n          <TabsList>\n            <TabsTrigger value=\"tab1\">Tab 1</TabsTrigger>\n            <TabsTrigger value=\"tab2\">Tab 2</TabsTrigger>\n            <TabsTrigger value=\"tab3\">Tab 3</TabsTrigger>\n          </TabsList>\n          <TabsContent value=\"tab1\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              First Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the first tab. You can put any React content here.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab2\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Second Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the second tab with different information.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab3\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Third Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              And this is the third tab with its own unique content.\n            </Typography>\n          </TabsContent>\n        </Tabs>\n\n        <Card\n          style={{\n            marginTop: '20px',\n          }}>\n          <Typography variant=\"body2\">\n            Active tab: <strong>{activeTab}</strong>\n          </Typography>\n          <div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab1')}>\n              Go to Tab 1\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab2')}>\n              Go to Tab 2\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab3')}>\n              Go to Tab 3\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const DisabledTab = {\n  render: () => (\n    <div style={{ width: '350px' }}>\n      <Tabs defaultValue=\"available\">\n        <TabsList>\n          <TabsTrigger value=\"available\">Available</TabsTrigger>\n          <TabsTrigger value=\"disabled\" disabled>\n            Disabled\n          </TabsTrigger>\n          <TabsTrigger value=\"another\">Another</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"available\">\n          <Typography variant=\"body1\">This tab is available and active.</Typography>\n        </TabsContent>\n        <TabsContent value=\"disabled\">\n          <Typography variant=\"body1\">This content should not be visible.</Typography>\n        </TabsContent>\n        <TabsContent value=\"another\">\n          <Typography variant=\"body1\">This is another available tab.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Tabs/Tabs.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ComponentProps, createContext, type ReactNode, use, useState } from 'react'\n\nimport s from './Tabs.module.css'\n\ntype TabsContextType = {\n  value?: string\n  onValueChange?: (value: string) => void\n}\n\nconst TabsContext = createContext<TabsContextType | null>(null)\n\nconst useTabsContext = () => {\n  const context = use(TabsContext)\n  if (!context) {\n    throw new Error('Tabs compound components must be used within Tabs component')\n  }\n  return context\n}\n\n/*\n * Tabs\n */\n\nexport type TabsProps = {\n  children: ReactNode\n  defaultValue?: string\n  value?: string\n  onValueChange?: (value: string) => void\n} & ComponentProps<'div'>\n\nexport const Tabs = ({\n  children,\n  defaultValue,\n  value: controlledValue,\n  onValueChange,\n  className,\n  ...props\n}: TabsProps) => {\n  const [internalValue, setInternalValue] = useState(defaultValue)\n\n  const isControlled = controlledValue !== undefined\n  const value = isControlled ? controlledValue : internalValue\n\n  const handleValueChange = (newValue: string) => {\n    if (!isControlled) {\n      setInternalValue(newValue)\n    }\n    onValueChange?.(newValue)\n  }\n\n  return (\n    <div className={className} {...props}>\n      <TabsContext value={{ value, onValueChange: handleValueChange }}>{children}</TabsContext>\n    </div>\n  )\n}\n\n/*\n * TabsList\n */\n\nexport type TabsListProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const TabsList = ({ children, className }: TabsListProps) => {\n  return <div className={clsx(s.tabsList, className)}>{children}</div>\n}\n\n/*\n * TabsTrigger\n */\n\nexport type TabsTriggerProps = {\n  children: ReactNode\n  value: string\n  className?: string\n  disabled?: boolean\n}\n\nexport const TabsTrigger = ({ children, value, className, disabled }: TabsTriggerProps) => {\n  const { value: activeValue, onValueChange } = useTabsContext()\n  const isActive = activeValue === value\n\n  const handleClick = () => {\n    if (!disabled) {\n      onValueChange?.(value)\n    }\n  }\n\n  return (\n    <button\n      className={clsx(s.tabsTrigger, isActive && s.active, disabled && s.disabled, className)}\n      onClick={handleClick}\n      disabled={disabled}\n      type=\"button\">\n      {children}\n    </button>\n  )\n}\n\n/*\n * TabsContent\n */\n\nexport type TabsContentProps = {\n  children: ReactNode\n  value: string\n  className?: string\n}\n\nexport const TabsContent = ({ children, value, className }: TabsContentProps) => {\n  const { value: activeValue } = useTabsContext()\n  const isActive = activeValue === value\n\n  if (!isActive) return null\n\n  return <div className={clsx(s.tabsContent, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Tabs/index.ts",
    "content": "export * from './Tabs'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/TagEditor/TagEditor.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n}\n\n.tagsContainer {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n\n  margin-top: 12px;\n  padding: 8px 0;\n}\n\n.tag {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n\n  padding: 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 16px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.deleteButton:enabled:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.counter {\n  margin-top: 8px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/TagEditor/TagEditor.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { TagEditor } from './TagEditor'\n\nconst meta = {\n  title: 'Components/TagEditor',\n  component: TagEditor,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TagEditor>\n\nexport default meta\n\nexport const Basic = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags\"\n          placeholder=\"Add tag and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Skills (max 5)\"\n          placeholder=\"Add skill and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={5}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [tags, setTags] = useState(['React', 'TypeScript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags (disabled)\"\n          placeholder=\"Cannot add tags\"\n          value={tags}\n          onTagsChange={setTags}\n          disabled={true}\n        />\n      </div>\n    )\n  },\n}\n\nexport const PrefilledTags = {\n  render: () => {\n    const [tags, setTags] = useState([\n      'JavaScript',\n      'TypeScript',\n      'React',\n      'Node.js',\n      'CSS',\n      'HTML',\n    ])\n\n    return (\n      <div style={{ width: '450px' }}>\n        <TagEditor\n          label=\"Programming Languages & Technologies\"\n          placeholder=\"Add more technologies...\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={10}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendTags, setFrontendTags] = useState(['React', 'Vue.js'])\n    const [backendTags, setBackendTags] = useState(['Node.js'])\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Frontend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Frontend\"\n            placeholder=\"Add frontend technology...\"\n            value={frontendTags}\n            onTagsChange={setFrontendTags}\n            maxTags={8}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Backend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Backend\"\n            placeholder=\"Add backend technology...\"\n            value={backendTags}\n            onTagsChange={setBackendTags}\n            maxTags={6}\n          />\n        </div>\n\n        <Card>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Summary:\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '4px' }}>\n            Frontend: {frontendTags.length > 0 ? frontendTags.join(', ') : 'None'}\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block' }}>\n            Backend: {backendTags.length > 0 ? backendTags.join(', ') : 'None'}\n          </Typography>\n        </Card>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/TagEditor/TagEditor.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, KeyboardEvent } from 'react'\nimport { useState } from 'react'\n\nimport { DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport s from './TagEditor.module.css'\n\nexport type TagEditorProps = {\n  label?: string\n  placeholder?: string\n  value: string[]\n  onTagsChange: (tags: string[]) => void\n  maxTags?: number\n  disabled?: boolean\n} & ComponentProps<'div'>\n\nexport const TagEditor = ({\n  label,\n  placeholder = 'Add tag and press Enter',\n  value,\n  onTagsChange,\n  className,\n  maxTags,\n  disabled = false,\n  ...props\n}: TagEditorProps) => {\n  const [inputValue, setInputValue] = useState('')\n\n  const addTag = (tag: string) => {\n    const trimmedTag = tag.trim()\n\n    if (!trimmedTag) return\n    if (value.includes(trimmedTag)) return\n    if (maxTags && value.length >= maxTags) return\n\n    onTagsChange([...value, trimmedTag])\n    setInputValue('')\n  }\n\n  const removeTag = (tagToRemove: string) => {\n    onTagsChange(value.filter((tag) => tag !== tagToRemove))\n  }\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      addTag(inputValue)\n    }\n\n    if (e.key === 'Backspace' && !inputValue && value.length > 0) {\n      removeTag(value[value.length - 1])\n    }\n  }\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <TextField\n        label={label}\n        value={inputValue}\n        onChange={(e) => setInputValue(e.target.value)}\n        onKeyDown={handleKeyDown}\n        placeholder={isMaxTagsReached ? 'Max tags reached' : placeholder}\n        disabled={disabled}\n      />\n\n      {value.length > 0 && (\n        <ul className={s.tagsContainer}>\n          {value.map((tag) => (\n            <li key={tag} className={s.tag}>\n              <Typography variant=\"body2\" className={s.tagText}>\n                {tag}\n              </Typography>\n              <IconButton\n                onClick={() => removeTag(tag)}\n                className={s.deleteButton}\n                disabled={disabled}\n                aria-label={`Remove tag ${tag}`}\n                type=\"button\">\n                <DeleteIcon />\n              </IconButton>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} tags\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/TagEditor/index.ts",
    "content": "export * from './TagEditor'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/TextField/TextField.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.icon {\n  position: absolute;\n  top: 50%;\n  left: 12px;\n  transform: translateY(-50%);\n\n  display: flex;\n\n  color: var(--color-text-secondary);\n}\n\n.input {\n  width: 100%;\n  height: 40px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input.large {\n  height: 56px;\n}\n\n.input:disabled {\n  color: var(--color-disabled);\n}\n\n.input:focus,\n.input:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.input:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input.error {\n  border-color: var(--color-text-error);\n}\n\n.input.withIcon {\n  padding-left: 40px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/TextField/TextField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport { TextField } from './TextField'\n\nconst meta = {\n  title: 'Components/TextField',\n  component: TextField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TextField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const Search: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    icon: <SearchIcon width={20} height={20} />,\n    inputSize: 'l',\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/TextField/TextField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './TextField.module.css'\n\nexport type TextFieldSize = 'm' | 'l'\n\nexport type TextFieldProps = {\n  errorMessage?: string\n  label?: ReactNode\n  icon?: ReactNode\n  inputSize?: TextFieldSize\n} & ComponentProps<'input'>\n\nexport const TextField = ({\n  className,\n  errorMessage,\n  id,\n  icon,\n  label,\n  inputSize = 'm',\n  ...props\n}: TextFieldProps) => {\n  const showError = Boolean(errorMessage)\n  const inputId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={inputId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.inputWrapper}>\n        {icon && <span className={s.icon}>{icon}</span>}\n        <input\n          className={clsx(\n            s.input,\n            showError && s.error,\n            icon && s.withIcon,\n            inputSize === 'l' && s.large\n          )}\n          id={inputId}\n          type={'text'}\n          {...props}\n        />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/TextField/index.ts",
    "content": "export * from './TextField'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Textarea/Textarea.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.textarea {\n  resize: none;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.textarea:disabled {\n  color: var(--color-disabled);\n}\n\n.textarea:focus,\n.textarea:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.textarea:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.textarea::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.textarea.error {\n  border-color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Textarea/Textarea.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Textarea } from './Textarea'\n\nconst meta = {\n  title: 'Components/Textarea',\n  component: Textarea,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Textarea>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const WithRows: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    rows: 5,\n  },\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Textarea/Textarea.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Textarea.module.css'\n\nexport type TextareaProps = {\n  errorMessage?: string\n  label?: ReactNode\n} & ComponentProps<'textarea'>\n\nexport const Textarea = ({ className, errorMessage, id, label, ...props }: TextareaProps) => {\n  const showError = Boolean(errorMessage)\n  const textareaId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={textareaId}>\n          {label}\n        </Typography>\n      )}\n\n      <textarea className={clsx(s.textarea, showError && s.error)} id={textareaId} {...props} />\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Textarea/index.ts",
    "content": "export * from './Textarea'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Typography/Typography.module.css",
    "content": ".label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.error {\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.h1 {\n  font-size: var(--font-size-xxxl);\n  line-height: 1.3;\n}\n\n.h2 {\n  margin: 0;\n  font-size: var(--font-size-xl);\n  font-weight: 600;\n  line-height: 1.3;\n}\n\n.h3 {\n  margin: 0;\n  font-size: var(--font-size-xs);\n  font-weight: 600;\n  line-height: 1.7;\n}\n\n.body1 {\n  margin: 0;\n  font-size: var(--font-size-l);\n  font-weight: 400;\n}\n\n.body2 {\n  margin: 0;\n  font-size: var(--font-size-m);\n  font-weight: 400;\n  color: var(--color-text-secondary);\n}\n\n.body3 {\n  margin: 0;\n  font-size: var(--font-size-xxs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* ------------------------------------------------------------ */\n\n.caption {\n  margin: 0;\n  font-size: 0.75rem;\n  font-weight: 400;\n  line-height: 1.66;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Typography/Typography.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from './Typography'\n\nconst meta = {\n  title: 'Components/Typography',\n  component: Typography,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Typography>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllTypography: Story = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>\n      <Typography variant=\"h1\">h1</Typography>\n      <Typography variant=\"h2\">h2</Typography>\n      <Typography variant=\"h3\">h3</Typography>\n      <Typography variant=\"body1\">body1</Typography>\n      <Typography variant=\"body2\">body2</Typography>\n      <Typography variant=\"caption\">caption</Typography>\n      <Typography variant=\"label\">label</Typography>\n      <Typography variant=\"error\">error</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Typography/Typography.tsx",
    "content": "import clsx from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\nimport React from 'react'\n\nimport styles from './Typography.module.css'\n\nconst VARIANT_DEFAULT_COMPONENT: Record<string, ElementType> = {\n  h1: 'h1',\n  h2: 'h2',\n  h3: 'h3',\n  body1: 'p',\n  body2: 'p',\n  body3: 'p',\n  caption: 'span',\n  label: 'label',\n}\n\ntype TypographyVariant =\n  | 'h1'\n  | 'h2'\n  | 'h3'\n  | 'body1'\n  | 'body2'\n  | 'body3'\n  | 'caption'\n  | 'label'\n  | 'error'\n\ntype Props<T extends ElementType> = {\n  variant?: TypographyVariant\n  as?: T\n  children: React.ReactNode\n} & ComponentProps<T>\n\nexport const Typography = <T extends ElementType = 'span'>({\n  variant = 'body1',\n  as,\n  children,\n  className = '',\n  ...props\n}: Props<T>) => {\n  const Component = as || VARIANT_DEFAULT_COMPONENT[variant] || 'span'\n  const variantClass = styles[variant] || ''\n\n  return (\n    <Component className={clsx(variantClass, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/Typography/index.ts",
    "content": "export * from './Typography'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/components/index.ts",
    "content": "export * from './AudioPlayer'\nexport * from './Autocomplete'\nexport * from './Avatar'\nexport * from './Button'\nexport * from './Card'\nexport * from './Dialog'\nexport * from './DropdownMenu'\nexport * from './FileUploader'\nexport * from './FormControlledTextField'\nexport * from './Hashtag'\nexport * from './IconButton'\nexport * from './ImageCropper'\nexport * from './ImageUploader'\nexport * from './Loader'\nexport * from './Pagination'\nexport * from './Progress'\nexport * from './ReactionButtons'\nexport * from './SearchField'\nexport * from './Select'\nexport * from './Skeleton'\nexport * from './Table'\nexport * from './Tabs'\nexport * from './TagEditor'\nexport * from './Textarea'\nexport * from './TextField'\nexport * from './Typography'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/configs/index.ts",
    "content": "export * from './paths'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/configs/paths.ts",
    "content": "export const Paths = {\n  Main: '/',\n  Auth: '/sign-in',\n  Playlists: '/playlists',\n  Profile: '/user',\n  Tracks: '/tracks',\n  TracksLyrics: '/tracks/lyrics',\n  OAuthRedirect: '/oauth/callback',\n  Artists: '/artists',\n  Tags: '/tags',\n  NotFound: '*',\n} as const\n"
  },
  {
    "path": "apps/rtk-query/src/shared/constants/constants.ts",
    "content": "export const LOCALE_KEY = 'locale'\n\n// SKELETON\nexport const TRACK_SKELETON_INFO_LINES = 3\nexport const TRACK_SKELETON_PLAYLISTS = 2\nexport const PLAYLIST_SKELETON_INFO_LINES = 4\nexport const PLAYLIST_SKELETON_TABLE_ROWS = 3\nexport const USER_TABS_SKELETON_PLAYLISTS = 5\n"
  },
  {
    "path": "apps/rtk-query/src/shared/constants/index.ts",
    "content": "export * from './constants.ts'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/hooks/index.ts",
    "content": "export * from './useAppDispatch'\nexport * from './useAppSelector'\nexport * from './useCurrentPage'\nexport * from './useDebounce'\nexport * from './useGetId'\nexport * from './useGlobalLoading'\nexport * from './useHover'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/hooks/useAppDispatch.ts",
    "content": "import { useDispatch } from 'react-redux'\n\nimport type { AppDispatch } from '@/app/store'\n\nexport const useAppDispatch = useDispatch.withTypes<AppDispatch>()\n"
  },
  {
    "path": "apps/rtk-query/src/shared/hooks/useAppSelector.ts",
    "content": "import { useSelector } from 'react-redux'\n\nimport type { RootState } from '@/app/store'\n\nexport const useAppSelector = useSelector.withTypes<RootState>()\n"
  },
  {
    "path": "apps/rtk-query/src/shared/hooks/useCurrentPage.ts",
    "content": "import { matchPath, useLocation } from 'react-router'\n\nimport { Paths } from '@/shared/configs'\n\nexport const useCurrentPage = () => {\n  const location = useLocation()\n\n  const isMainPage = matchPath({ path: Paths.Main }, location.pathname)\n  const isTracksPage = matchPath({ path: Paths.Tracks }, location.pathname)\n  const isTrackPage = matchPath({ path: `${Paths.Tracks}/:id` }, location.pathname)\n  const isTrackLyricsPage = matchPath({ path: `${Paths.TracksLyrics}/:id` }, location.pathname)\n  const isPlaylistsPage = matchPath({ path: Paths.Playlists }, location.pathname)\n  const isPlaylistPage = matchPath({ path: `${Paths.Playlists}/:id` }, location.pathname)\n  const isUserPage = matchPath({ path: `${Paths.Profile}/:userId` }, location.pathname)\n\n  return {\n    isMainPage: !!isMainPage,\n    isTracksPage: !!isTracksPage,\n    isTrackPage: !!isTrackPage,\n    isTrackLyricsPage: !!isTrackLyricsPage,\n    isPlaylistsPage: !!isPlaylistsPage,\n    isPlaylistPage: !!isPlaylistPage,\n    isUserPage: !!isUserPage,\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/hooks/useDebounce.ts",
    "content": "import { useEffect, useState } from 'react'\n\n/**\n * hook that returns a value after a specified time\n * */\nexport function useDebounce<T>(value: T, delay?: number): T {\n  const [debouncedValue, setDebouncedValue] = useState<T>(value)\n\n  useEffect(() => {\n    const timer = setTimeout(() => setDebouncedValue(value), delay || 500)\n\n    return () => {\n      clearTimeout(timer)\n    }\n  }, [value, delay])\n\n  return debouncedValue\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/hooks/useGetId.ts",
    "content": "import { useId } from 'react'\n\n/*\n * Custom hook to get an ID.\n * If an ID is passed from component props, it returns that ID.\n * Otherwise, it generates and returns a new unique ID.\n *\n * @param {string} [idFromComponentProps] - An optional ID passed from ComponentProps.\n * @returns {string} The ID from component props or a generated unique ID.\n */\nexport const useGetId = (idFromComponentProps?: string) => {\n  const generatedId = useId()\n\n  return idFromComponentProps || generatedId\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/hooks/useGlobalLoading.ts",
    "content": "import { useAppSelector } from './useAppSelector'\n\nexport const useIsGlobalLoading = () => {\n  return useAppSelector((state) => {\n    const queries = state.baseApi.queries\n    const mutations = state.baseApi.mutations\n    const isLoadingQueries = Object.values(queries).some((q) => q?.status === 'pending')\n    const isLoadingMutations = Object.values(mutations).some((m) => m?.status === 'pending')\n\n    return isLoadingQueries || isLoadingMutations\n  })\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/hooks/useHover.ts",
    "content": "import { type RefObject, useEffect, useRef, useState } from 'react'\n\nexport function useHover<T extends HTMLElement>(): [RefObject<T | null>, boolean] {\n  const [hover, setHover] = useState(false)\n  const ref = useRef<T | null>(null)\n\n  useEffect(() => {\n    const node = ref.current\n    if (!node) return\n\n    const handleMouseEnter = () => setHover(true)\n    const handleMouseLeave = () => setHover(false)\n\n    node.addEventListener('mouseenter', handleMouseEnter)\n    node.addEventListener('mouseleave', handleMouseLeave)\n\n    return () => {\n      node.removeEventListener('mouseenter', handleMouseEnter)\n      node.removeEventListener('mouseleave', handleMouseLeave)\n    }\n  }, [])\n\n  return [ref, hover]\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/AddToPlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const AddToPlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <path\n      fill=\"#000\"\n      d=\"M8.134 4.795v2.456h2.34v.776h-2.34V10.5h-.84V8.026H4.966v-.776h2.328V4.795h.84Z\"\n    />\n    <path\n      fill=\"#fff\"\n      d=\"M5.89 16.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.665 2.333 2.333 0 0 0 0-4.665ZM17.89 14.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.666 2.333 2.333 0 0 0 0-4.666ZM10.902 5.9l10.489-1.998v1l-10.5 2 .011-1.003Z\"\n    />\n    <path fill=\"#fff\" d=\"M8.39 11.5h1v8l-1-.533V11.5ZM20.39 4.964l1-.464v13l-1-.928V4.963Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/AddTrackIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const AddTrackIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 16 16\"\n    width={16}\n    height={16}\n    fill=\"none\"\n    {...props}>\n    <circle cx={5.26} cy={4.667} r={3.667} fill=\"currentColor\" />\n    <circle cx={5.26} cy={4.667} r={3.667} fill=\"currentColor\" />\n    <path\n      fill=\"#000\"\n      d=\"M6.99 5.018H5.424v1.649H4.87V5.018H3.313v-.517h1.556V2.864h.556V4.5H6.99v.517Z\"\n    />\n    <path\n      fill=\"currentColor\"\n      d=\"M3.927 11a2.334 2.334 0 1 1 0 4.668 2.334 2.334 0 0 1 0-4.668Zm0 .778a1.555 1.555 0 1 0 0 3.111 1.555 1.555 0 0 0 0-3.11ZM11.927 9.667a2.334 2.334 0 1 1 0 4.668 2.334 2.334 0 0 1 0-4.668Zm0 .777a1.556 1.556 0 1 0 .001 3.112 1.556 1.556 0 0 0 0-3.112ZM7.267 3.933l6.992-1.331v.666l-7 1.334.008-.669Z\"\n    />\n    <path\n      fill=\"currentColor\"\n      d=\"M5.594 7.667h.666V13l-.666-.355V7.667ZM13.594 3.31 14.26 3v8.667l-.666-.62V3.31Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/ArrowBackIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ArrowBackIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"40\"\n    height=\"40\"\n    viewBox=\"0 0 40 40\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      d=\"M33.3337 18.3332H13.0503L22.367 9.0165L20.0003 6.6665L6.66699 19.9998L20.0003 33.3332L22.3503 30.9832L13.0503 21.6665H33.3337V18.3332Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/ArrowDownIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ArrowDownIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={20}\n    height={20}\n    viewBox=\"0 0 20 20\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M6.175 7.158 10 10.975l3.825-3.817L15 8.333l-5 5-5-5 1.175-1.175Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/CheckedIcon.tsx",
    "content": "import React from 'react'\n\nexport const CheckedIcon = () => (\n  <svg width={18} height={18} viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M16 0H2C0.89 0 0 0.9 0 2V16C0 17.1 0.89 18 2 18H16C17.11 18 18 17.1 18 16V2C18 0.9 17.11 0 16 0ZM7 14L2 9L3.41 7.59L7 11.17L14.59 3.58L16 5L7 14Z\"\n      fill=\"white\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/ClockIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ClockIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    viewBox=\"0 0 28 28\"\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14 3c6.075 0 11 4.925 11 11s-4.925 11-11 11S3 20.075 3 14 7.925 3 14 3Zm0 2a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm.5 8.5H18v2h-5.5v-7h2v5Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M0 0h28v28H0z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/CreateIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const CreateIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M16 2.667C8.64 2.667 2.667 8.64 2.667 16S8.64 29.333 16 29.333 29.333 23.36 29.333 16 23.36 2.666 16 2.666Zm6.667 14.666h-5.334v5.334h-2.666v-5.334H9.333v-2.666h5.334V9.332h2.666v5.333h5.334v2.667Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/DeleteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DeleteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={10}\n    height={12}\n    viewBox=\"0 0 10 12\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M7.333 4.25v5.833H2.666V4.25h4.667ZM6.458.75H3.54l-.583.583H.916V2.5h8.167V1.333H7.04L6.458.75Zm2.041 2.333h-7v7a1.17 1.17 0 0 0 1.167 1.167h4.667a1.17 1.17 0 0 0 1.166-1.167v-7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/DeleteTagIconButton.tsx",
    "content": "import React from 'react'\n\nexport const DeleteTagIconButton = () => (\n  <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M6.66667 0C2.98 0 0 2.98 0 6.66667C0 10.3533 2.98 13.3333 6.66667 13.3333C10.3533 13.3333 13.3333 10.3533 13.3333 6.66667C13.3333 2.98 10.3533 0 6.66667 0ZM10 9.06L9.06 10L6.66667 7.60667L4.27333 10L3.33333 9.06L5.72667 6.66667L3.33333 4.27333L4.27333 3.33333L6.66667 5.72667L9.06 3.33333L10 4.27333L7.60667 6.66667L10 9.06Z\"\n      fill=\"#A9A9A9\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/DislikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DislikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 28 28\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-1.12 0-2.217.292-3.185.805L14 10.5h3.5L14 22.167l1.167-10.5h-3.5l1.796-6.289C12.215 4.212 10.512 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.818 4.854 8.376 11.667 14.583 6.382-5.763 11.667-9.637 11.667-14.583 0-3.594-2.824-6.417-6.417-6.417Zm-7.303 16.018c-4.422-3.955-7.28-6.685-7.28-9.601A4.044 4.044 0 0 1 8.75 5.833c.688 0 1.388.175 2.018.49L8.575 14h3.99l-.618 5.518Zm5.705-1.4 2.986-9.951h-3.395l.712-2.124c.42-.14.863-.21 1.295-.21a4.044 4.044 0 0 1 4.083 4.084c0 2.578-2.356 5.168-5.681 8.201Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/DownloadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DownloadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={22}\n    height={22}\n    viewBox=\"0 0 22 22\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M11.733 14.164V5.867h-1.466v8.286l-2.822-3.28-1.112.954 4.668 5.43 4.687-5.427-1.112-.958-2.843 3.292ZM11 0C4.925 0 0 4.925 0 11s4.925 11 11 11 11-4.925 11-11S17.075 0 11 0Zm0 20.533c-5.257 0-9.533-4.277-9.533-9.533 0-5.257 4.276-9.533 9.533-9.533 5.256 0 9.533 4.276 9.533 9.533 0 5.256-4.277 9.533-9.533 9.533Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/EditIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const EditIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m13.888 9.517.844.766-8.305 7.55h-.844v-.766l8.305-7.55Zm3.3-5.017a.966.966 0 0 0-.641.242l-1.678 1.525 3.438 3.125 1.677-1.525a.778.778 0 0 0 0-1.175l-2.145-1.95a.949.949 0 0 0-.65-.242Zm-3.3 2.658L3.75 16.375V19.5h3.438l10.138-9.217-3.438-3.125Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/HomeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const HomeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      d=\"M16.0001 7.58667L22.6667 13.5867V24H20.0001V16H12.0001V24H9.33341V13.5867L16.0001 7.58667ZM16.0001 4L2.66675 16H6.66675V26.6667H14.6667V18.6667H17.3334V26.6667H25.3334V16H29.3334L16.0001 4Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/IconOneRepeat.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const IconOneRepeat = (props: SVGProps<SVGSVGElement>) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"96\" height=\"96\" viewBox=\"0 0 23 23\" {...props}>\n      <rect width=\"24\" height=\"24\" fill=\"none\" />\n      <path\n        fill=\"none\"\n        fillOpacity={0.7}\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"2\"\n        d=\"m16 4l3 3H5v3m3 10l-3-3h14v-3m-9-2.5l2-1.5v4\"\n      />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/ImageUploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ImageUploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={35}\n    height={34}\n    fill=\"none\"\n    viewBox=\"0 0 35 34\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M30.834 3.667v20h-20v-20h20Zm0-3.334h-20a3.343 3.343 0 0 0-3.333 3.334v20C7.5 25.5 9 27 10.834 27h20c1.833 0 3.333-1.5 3.333-3.333v-20c0-1.834-1.5-3.334-3.333-3.334ZM16.667 16.45l2.817 3.767 4.133-5.167 5.55 6.95H12.501l4.166-5.55ZM.834 7v23.333c0 1.834 1.5 3.334 3.333 3.334h23.334v-3.334H4.167V7H.834Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/KeyboardArrowLeftIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowLeftIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path fill=\"currentColor\" d=\"M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/KeyboardArrowRightIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowRightIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width={24} height={24} fill=\"none\" {...props}>\n    <path fill=\"#fff\" d=\"M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LanguageIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LanguageIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    height=\"24\"\n    fill=\"none\"\n    stroke=\"#fff\"\n    strokeWidth=\"0.75\"\n    {...props}>\n    <circle cx=\"12\" cy=\"12\" r=\"10\" />\n    <path d=\"M12,22 C14.6666667,19.5757576 16,16.2424242 16,12 C16,7.75757576 14.6666667,4.42424242 12,2 C9.33333333,4.42424242 8,7.75757576 8,12 C8,16.2424242 9.33333333,19.5757576 12,22 Z\" />\n    <path d=\"M2.5 9H21.5M2.5 15H21.5\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LibraryIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LibraryIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"  currentColor\"\n      d=\"M26.667 2.667h-16A2.674 2.674 0 0 0 8 5.332v16C8 22.8 9.2 24 10.667 24h16c1.466 0 2.666-1.2 2.666-2.667v-16c0-1.467-1.2-2.667-2.666-2.667Zm0 16.666a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12Zm-10 .667A3.335 3.335 0 0 0 20 16.666v-5.333a2 2 0 0 1 2-2h.667a1.333 1.333 0 1 0 0-2.667h-2a2 2 0 0 0-2 2v3.196c0 .882-1.119 1.471-2 1.471a3.334 3.334 0 0 0 0 6.667ZM5.333 9.333a1.333 1.333 0 1 0-2.666 0v17.333c0 1.467 1.2 2.667 2.666 2.667h17.334a1.333 1.333 0 0 0 0-2.666H7.333a2 2 0 0 1-2-2V9.332Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    viewBox=\"0 0 28 28\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-2.03 0-3.978.945-5.25 2.438C12.728 4.445 10.78 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.41 3.967 8.003 9.975 13.463L14 24.908l1.692-1.54c6.008-5.448 9.975-9.041 9.975-13.451 0-3.594-2.824-6.417-6.417-6.417Zm-5.133 18.142-.117.116-.117-.116C8.33 16.613 4.667 13.288 4.667 9.917c0-2.334 1.75-4.084 4.083-4.084 1.797 0 3.547 1.155 4.165 2.754h2.182c.606-1.599 2.356-2.754 4.153-2.754 2.333 0 4.083 1.75 4.083 4.084 0 3.371-3.663 6.696-9.216 11.725Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LikeIconFill.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIconFill = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 29 28\"\n    width={29}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14.4 6.04a6.137 6.137 0 0 1 8.655.248c2.375 2.47 2.457 6.402.247 8.967L14.4 24.5l-8.902-9.245c-2.21-2.566-2.126-6.504.248-8.967C8.123 3.823 11.927 3.74 14.4 6.04Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M.4 0h28v28H.4z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LikeInSquareIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeInSquareIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <rect width={32} height={32} fill=\"url(#a)\" rx={2} />\n    <path\n      fill=\"#fff\"\n      d=\"M16 10.158c1.645-1.597 4.186-1.544 5.77.173 1.583 1.717 1.638 4.453.165 6.237L16 23l-5.934-6.432c-1.473-1.784-1.418-4.524.165-6.237 1.585-1.715 4.121-1.773 5.77-.173Z\"\n    />\n    <defs>\n      <linearGradient id=\"a\" x1={1} x2={32} y1={1} y2={30.5} gradientUnits=\"userSpaceOnUse\">\n        <stop stopColor=\"#3822EA\" />\n        <stop offset={1} stopColor=\"#C7E9D7\" />\n      </linearGradient>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LiveWaveIcon/LiveWaveIcon.module.css",
    "content": ".bar {\n  transform-origin: center bottom;\n  animation: wave 1.2s ease-in-out infinite alternate;\n}\n\n@keyframes wave {\n  0% {\n    transform: scaleY(0.4);\n  }\n\n  50% {\n    transform: scaleY(1);\n  }\n\n  100% {\n    transform: scaleY(0.6);\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LiveWaveIcon/LiveWaveIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nimport s from './LiveWaveIcon.module.css'\n\nexport const LiveWaveIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <rect\n      x={2}\n      y={8}\n      width={2}\n      height={8}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '0ms' }}\n    />\n    <rect\n      x={6}\n      y={4}\n      width={2}\n      height={16}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '150ms' }}\n    />\n    <rect\n      x={10}\n      y={6}\n      width={2}\n      height={12}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '300ms' }}\n    />\n    <rect\n      x={14}\n      y={2}\n      width={2}\n      height={20}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '450ms' }}\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LiveWaveIcon/index.ts",
    "content": "export { LiveWaveIcon } from './LiveWaveIcon'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/LogoutIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const LogoutIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m17 8-1.41 1.41L17.17 11H9v2h8.17l-1.58 1.58L17 16l4-4-4-4ZM5 5h7V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h7v-2H5V5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/MoreIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const MoreIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={16}\n    height={4}\n    viewBox=\"0 0 16 4\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M2 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM8 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM16 2a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/PauseIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PauseIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 40 40\"\n    width={40}\n    height={40}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"#fff\"\n      d=\"M20 0c11.046 0 20 8.954 20 20s-8.954 20-20 20S0 31.046 0 20 8.954 0 20 0Zm-6 11a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Zm9 0a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/PlayIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlayIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={72}\n    height={72}\n    viewBox=\"0 0 72 72\"\n    fill=\"none\"\n    {...props}>\n    <circle cx={36} cy={36} r={36} fill=\"#FF38B6\" />\n    <path\n      fill=\"#000\"\n      d=\"M49.287 36.512c.865-.486.865-1.7 0-2.186l-19.47-10.93c-.864-.485-1.946.122-1.946 1.093v21.86c0 .971 1.082 1.579 1.947 1.093l19.469-10.93Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/PlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M28 4H4a2.675 2.675 0 0 0-2.667 2.667v18.666C1.333 26.8 2.533 28 4 28h24c1.467 0 2.667-1.2 2.667-2.667V6.667C30.667 5.2 29.467 4 28 4Zm0 21.333H4V6.667h24v18.666ZM10.667 20c0-2.213 1.786-4 4-4 .466 0 .92.093 1.333.24V8h6.667v2.667h-4v9.373a4.003 4.003 0 0 1-4 3.96c-2.214 0-4-1.787-4-4Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/PlusIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlusIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <path\n      fill=\"var(--color-text-secondary)\"\n      d=\"M30 0a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h28ZM15 9v6H9v2h6v6h2v-6h6v-2h-6V9h-2Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/ProfileIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const ProfileIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2Zm0 16h-4.83L12 20.17 9.83 18H5V4h14v14Zm-7-7c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3Zm0-4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1Zm6 8.58c0-2.5-3.97-3.58-6-3.58s-6 1.08-6 3.58V17h12v-1.42ZM8.48 15c.74-.51 2.23-1 3.52-1s2.78.49 3.52 1H8.48Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/RepeatIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const RepeatIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M9.333 9.333h13.334v4L28 8l-5.333-5.333v4h-16v8h2.666V9.332Zm13.334 13.333H9.333v-4L4 24l5.333 5.333v-4h16v-8h-2.666v5.334Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/SearchIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SearchIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 32 32\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m23.775 22.356 5.817 6.137c.56.59.541 1.534-.04 2.1a1.414 1.414 0 0 1-2.024-.04l-5.822-6.145c-1.979 1.522-4.21 2.36-6.695 2.512a11.872 11.872 0 0 1-4.822-.691c-1.556-.563-2.912-1.366-4.07-2.41-1.159-1.042-2.107-2.313-2.843-3.813a12.37 12.37 0 0 1-1.254-4.779 12.41 12.41 0 0 1 .687-4.898c.557-1.58 1.35-2.958 2.378-4.136 1.028-1.177 2.281-2.14 3.76-2.89a11.915 11.915 0 0 1 4.707-1.28c1.66-.102 3.268.129 4.823.692 1.555.563 2.912 1.366 4.07 2.409 1.159 1.043 2.106 2.314 2.843 3.814a12.368 12.368 0 0 1 1.253 4.779 12.567 12.567 0 0 1-.21 3.162 12.259 12.259 0 0 1-.958 2.929 12.892 12.892 0 0 1-1.6 2.548Zm-8.935 1.635a9.024 9.024 0 0 0 3.596-.982 9.525 9.525 0 0 0 2.869-2.216c.786-.9 1.394-1.952 1.823-3.156a9.4 9.4 0 0 0 .53-3.743 9.367 9.367 0 0 0-.963-3.65c-.566-1.143-1.292-2.113-2.178-2.91a9.443 9.443 0 0 0-3.106-1.847 8.992 8.992 0 0 0-3.685-.534 9.025 9.025 0 0 0-3.596.982A9.524 9.524 0 0 0 7.26 8.15c-.785.9-1.393 1.953-1.822 3.157a9.4 9.4 0 0 0-.53 3.742 9.367 9.367 0 0 0 .962 3.65c.567 1.144 1.293 2.114 2.179 2.91a9.443 9.443 0 0 0 3.106 1.848 8.994 8.994 0 0 0 3.685.534Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/ShuffleIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ShuffleIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M14.12 12.227 7.213 5.333l-1.88 1.88 6.893 6.894 1.894-1.88Zm5.213-6.894 2.72 2.72-16.72 16.734 1.88 1.88 16.733-16.72 2.72 2.72V5.334h-7.333Zm.44 12.547-1.88 1.88 4.173 4.173-2.733 2.734h7.333v-7.334l-2.72 2.72-4.173-4.173Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/SkipNextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipNextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"m8 24 11.333-8L8 8v16Zm2.667-10.853L14.707 16l-4.04 2.853v-5.706ZM21.333 8H24v16h-2.667V8Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/SkipPreviousIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipPreviousIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M8 8h2.667v16H8V8Zm4.667 8L24 24V8l-11.333 8Zm8.666 2.853L17.293 16l4.04-2.853v5.706Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/StaticWaveIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const StaticWaveIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"28\"\n    height=\"28\"\n    viewBox=\"0 0 28 28\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <g clipPath=\"url(#clip0_24878_2110)\">\n      <rect x=\"6\" y=\"5\" width=\"3\" height=\"18\" fill=\"#FF38B6\" />\n      <rect x=\"10\" y=\"19\" width=\"3\" height=\"4\" fill=\"#FF38B6\" />\n      <rect x=\"14\" y=\"9\" width=\"3\" height=\"14\" fill=\"#FF38B6\" />\n      <rect x=\"18\" y=\"19\" width=\"3\" height=\"4\" fill=\"#FF38B6\" />\n    </g>\n    <defs>\n      <clipPath id=\"clip0_24878_2110\">\n        <rect width=\"28\" height=\"28\" fill=\"white\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/TextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M14.17 5 19 9.83V19H5V5h9.17Zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V9.83c0-.53-.21-1.04-.59-1.41l-4.83-4.83c-.37-.38-.88-.59-1.41-.59ZM7 15h10v2H7v-2Zm0-4h10v2H7v-2Zm0-4h7v2H7V7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/TrackIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TrackIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m16 4 .013 14.067a5.329 5.329 0 0 0-2.666-.734A5.335 5.335 0 0 0 8 22.667 5.335 5.335 0 0 0 13.347 28c2.96 0 5.32-2.387 5.32-5.333V9.333H24V4h-8Zm-2.653 21.333a2.674 2.674 0 0 1-2.667-2.666c0-1.467 1.2-2.667 2.667-2.667 1.466 0 2.666 1.2 2.666 2.667 0 1.466-1.2 2.666-2.666 2.666Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/UncheckedIcon.tsx",
    "content": "import React from 'react'\n\nexport const UncheckedIcon = () => (\n  <svg width={18} height={18} viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M16 2V16H2V2H16ZM16 0H2C0.9 0 0 0.9 0 2V16C0 17.1 0.9 18 2 18H16C17.1 18 18 17.1 18 16V2C18 0.9 17.1 0 16 0Z\"\n      fill=\"white\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/UploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const UploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M24 20v4H8v-4H5.333v4c0 1.467 1.2 2.667 2.667 2.667h16c1.467 0 2.667-1.2 2.667-2.667v-4H24ZM9.333 12l1.88 1.88 3.454-3.44v10.894h2.666V10.44l3.454 3.44 1.88-1.88L16 5.333 9.333 12Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/VolumeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M4 12v8h5.333L16 26.667V5.333L9.333 12H4Zm9.333-.227v8.454l-2.893-2.894H6.667v-2.666h3.773l2.893-2.894ZM22 16a6 6 0 0 0-3.333-5.373V21.36A5.965 5.965 0 0 0 22 16ZM18.667 4.307v2.746C22.52 8.2 25.333 11.773 25.333 16c0 4.227-2.813 7.8-6.666 8.947v2.746C24.013 26.48 28 21.707 28 16S24.013 5.52 18.667 4.307Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/VolumeMuteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeMuteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width={24} height={24} {...props}>\n    <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n    <g fill=\"currentColor\">\n      <path d=\"M16.25 13.42c.15-.45.25-.92.25-1.42A4.5 4.5 0 0 0 14 7.97v3.2l2.25 2.25z\" />\n      <path d=\"M19 12c0 1.21-.31 2.34-.85 3.32l1.46 1.46A8.973 8.973 0 0 0 21 12c0-3.83-2.4-7.11-5.78-8.4-.59-.23-1.22.23-1.22.86v.19c0 .38.25.71.61.85C17.18 6.54 19 9.06 19 12zM2.1 3.51a.996.996 0 0 0 0 1.41L6.17 9H4c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71v-2.76l3.32 3.32c-.23.13-.47.24-.71.35-.37.16-.6.52-.6.91 0 .7.7 1.2 1.35.94.5-.2.99-.45 1.44-.73l2.28 2.28a.996.996 0 1 0 1.41-1.41L3.51 3.51a.996.996 0 0 0-1.41 0zM12 9.17V6.41c0-.89-1.08-1.34-1.71-.71l-.88.89L12 9.17z\" />\n    </g>\n  </svg>\n)\n"
  },
  {
    "path": "apps/rtk-query/src/shared/icons/index.ts",
    "content": "export * from './AddToPlaylistIcon'\nexport * from './AddTrackIcon'\nexport * from './ArrowDownIcon'\nexport * from './CheckedIcon'\nexport * from './ClockIcon'\nexport * from './CreateIcon'\nexport * from './DeleteIcon'\nexport * from './DeleteTagIconButton'\nexport * from './DislikeIcon'\nexport * from './DownloadIcon'\nexport * from './EditIcon'\nexport * from './HomeIcon'\nexport * from './ImageUploadIcon'\nexport * from './KeyboardArrowLeftIcon'\nexport * from './KeyboardArrowRightIcon'\nexport * from './LanguageIcon'\nexport * from './LibraryIcon'\nexport * from './LikeIcon'\nexport * from './LikeIconFill'\nexport * from './LikeInSquareIcon'\nexport * from './LiveWaveIcon'\nexport * from './LogoutIcon'\nexport * from './MoreIcon'\nexport * from './PauseIcon'\nexport * from './PlayIcon'\nexport * from './PlaylistIcon'\nexport * from './PlusIcon'\nexport * from './ProfileIcon'\nexport * from './RepeatIcon'\nexport * from './SearchIcon'\nexport * from './ShuffleIcon'\nexport * from './SkipNextIcon'\nexport * from './SkipPreviousIcon'\nexport * from './StaticWaveIcon'\nexport * from './TextIcon'\nexport * from './TrackIcon'\nexport * from './UncheckedIcon'\nexport * from './UploadIcon'\nexport * from './VolumeIcon'\nexport * from './VolumeMuteIcon'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/translations/i18nConfiguration.ts",
    "content": "import i18n from 'i18next'\nimport LanguageDetector from 'i18next-browser-languagedetector'\nimport { initReactI18next } from 'react-i18next'\n\nimport translationEN from './languages/en.json'\nimport translationRu from './languages/ru.json'\n\nconst defaultLanguage = localStorage.getItem('locale') || 'en'\n\nconst resources = {\n  en: {\n    translation: translationEN,\n  },\n  ru: {\n    translation: translationRu,\n  },\n}\n\ni18n\n  .use(LanguageDetector)\n  .use(initReactI18next)\n  .init({\n    resources,\n    lng: defaultLanguage,\n    fallbackLng: 'en',\n    interpolation: {\n      escapeValue: false,\n    },\n  })\n\nexport default i18n\n"
  },
  {
    "path": "apps/rtk-query/src/shared/translations/languages/en.json",
    "content": "{\n  \"artists\": {\n    \"label\": \"Artists\",\n    \"placeholder\": \"Search by artists\"\n  },\n  \"auth\": {\n    \"button\": {\n      \"continue_without_sign_in\": \"Continue without Sign in\",\n      \"sign_in\": \"Sign in\",\n      \"sign_in_with_apihub\": \"Sign in with APIHub\"\n    },\n    \"title\": {\n      \"logout\": \"Logout\",\n      \"my_profile\": \"My Profile\"\n    }\n  },\n  \"button\": {\n    \"choose\": \"Choose\",\n    \"create\": \"Create\",\n    \"edit\": \"Edit\",\n    \"delete\": \"Delete\",\n    \"cancel\": \"Cancel\",\n    \"update\": \"Update\",\n    \"edit_profile\": \"Edit profile\",\n    \"saving\": \"Saving...\",\n    \"save_changes\": \"Save Changes\"\n  },\n  \"description\": {\n    \"label\": {\n      \"description\": \"Description\"\n    },\n    \"title\": {\n      \"max_value\": \"Description must be less than {{ quantity }} characters\"\n    }\n  },\n  \"playlists\": {\n    \"button\": {\n      \"choose_playlist\": \"Choose playlist\",\n      \"create_playlist\": \"Create playlist\",\n      \"edit\": \"Edit\"\n    },\n    \"placeholder\": {\n      \"enter_playlist_description\": \"Enter playlist description\",\n      \"enter_playlist_title\": \"Enter playlist title\",\n      \"search_playlist\": \"Search playlist\"\n    },\n    \"title\": {\n      \"all_playlists\": \"All Playlists\",\n      \"create_playlist\": \"Create Playlist\",\n      \"edit_playlist\": \"Edit Playlist\",\n      \"new_playlists\": \"New playlists\",\n      \"playlists_not_found\": \"No playlists found\"\n    },\n    \"label\": {\n      \"load_error\": \"Unable to load the playlist\"\n    },\n    \"aria_labels\": {\n      \"open_playlist\": \"Open playlist {{title}}\"\n    }\n  },\n  \"sidebar\": {\n    \"all_playlists\": \"All Playlists\",\n    \"all_tracks\": \"All Tracks\",\n    \"create_playlist\": \"Create Playlist\",\n    \"home\": \"Home\",\n    \"upload_track\": \"Upload Track\",\n    \"your_library\": \"Your Library\"\n  },\n  \"tabs\": {\n    \"playlists\": \"Playlists\",\n    \"tracks\": \"Tracks\",\n    \"liked_playlists\": \"Liked Playlists\",\n    \"liked_tracks\": \"Liked Tracks\",\n    \"possessive_case\": \"'s\"\n  },\n  \"tags\": {\n    \"label\": \"Hashtags\",\n    \"placeholder\": \"Search by hashtags\"\n  },\n  \"title\": {\n    \"max_value\": \"Title must be less than {{ quantity }} characters\",\n    \"min_value\": \"Title must be at least {{ quantity }} characters\",\n    \"required\": \"Title is required\",\n    \"title\": \"Title\"\n  },\n  \"tracks\": {\n    \"button\": {\n      \"add_to_playlist\": \"Add to playlist\",\n      \"all_tracks\": \"All Tracks\",\n      \"cancel\": \"Cancel\",\n      \"create\": \"Create\",\n      \"edit\": \"Edit\",\n      \"delete\": \"Delete\",\n      \"save\": \"Save\",\n      \"show_text_song\": \"Show text song\",\n      \"upload\": \"Upload\",\n      \"choose_track\": \"Choose track\",\n      \"upload_track\": \"Upload track\",\n      \"go_back\": \"Go back\",\n      \"delete_from_playlist\": \"Delete from playlist\",\n      \"publish\": \"Publish\",\n      \"draft\": \"Draft\"\n    },\n    \"label\": {\n      \"lyrics\": \"Lyrics\",\n      \"title\": \"Title\",\n      \"no_tracks\": \"No tracks\",\n      \"load_error\": \"Unable to load the track\"\n    },\n    \"placeholder\": {\n      \"lyrics\": \"Enter track lyrics\",\n      \"search_tracks\": \"Search tracks\",\n      \"title\": \"Enter track title\",\n      \"no_lyrics\": \"No lyrics available for this track\"\n    },\n    \"title\": {\n      \"all_tracks\": \"All tracks\",\n      \"create\": \"Create Track\",\n      \"edit\": \"Edit Track\",\n      \"new_tracks\": \"New tracks\",\n      \"tracks_not_found\": \"Tracks not found\"\n    },\n    \"table\": {\n      \"track\": \"Track\",\n      \"date_added\": \"Date added\",\n      \"actions\": \"Actions\",\n      \"duration\": \"Duration\"\n    },\n    \"release\": \"Release date\"\n  },\n  \"sort\": {\n    \"label\": \"Sort by\",\n    \"newest_first\": \"Newest first\",\n    \"oldest_first\": \"Oldest first\",\n    \"most_liked\": \"Most liked\",\n    \"least_liked\": \"Least liked\"\n  },\n  \"placeholder\": {\n    \"upload_cover_image\": \"Upload Cover Image\",\n    \"search_and_select\": \"Search and select...\",\n    \"selected\": \"selected\",\n    \"no_options_found\": \"No options found\",\n    \"all_options_selected\": \"All options selected\",\n    \"which_playlist\": \"In which playlist is the track?\"\n  },\n  \"profile\": {\n    \"title\": {\n      \"edit_profile\": \"Edit profile\",\n      \"max_value_name\": \"Name must be less than {{ quantity }} characters\",\n      \"min_value_name\": \"Name must be at least {{ quantity }} characters\",\n      \"max_value_surname\": \"Surname must be less than {{ quantity }} characters\",\n      \"min_value_surname\": \"Surname must be at least {{ quantity }} characters\",\n      \"required_name\": \"Name is required\",\n      \"required_surname\": \"Surname is required\"\n    },\n    \"placeholder\": {\n      \"enter_profile_name\": \"Enter profile name\",\n      \"enter_profile_surname\": \"Enter profile surname\",\n      \"upload_avatar\": \"Upload Avatar\"\n    },\n    \"label\": {\n      \"name\": \"Name\",\n      \"surname\": \"Surname\"\n    },\n    \"stats\": {\n      \"playlists_one\": \"Playlist\",\n      \"playlists_other\": \"Playlists\",\n      \"tracks_one\": \"Track\",\n      \"tracks_other\": \"Tracks\"\n    }\n  },\n  \"date\": {\n    \"today\": \"today\",\n    \"dayAgo\": \"{{addedAt}} day ago\",\n    \"daysAgo\": \"{{addedAt}} days ago\",\n    \"monthAgo\": \"{{addedAt}} month ago\",\n    \"monthsAgo\": \"{{addedAt}} months ago\",\n    \"created\": \"Created\"\n  },\n  \"playlist\": {\n    \"made_for\": \"Made for\",\n    \"tracks_count_one\": \"{{count}} track\",\n    \"tracks_count_other\": \"{{count}} tracks\"\n  },\n  \"player\": {\n    \"unknown_artist\": \"Unknown Artist\"\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/translations/languages/ru.json",
    "content": "{\n  \"artists\": {\n    \"label\": \"Артисты\",\n    \"placeholder\": \"Поиск по артистам\"\n  },\n  \"auth\": {\n    \"button\": {\n      \"continue_without_sign_in\": \"Продолжить без входа\",\n      \"sign_in\": \"Войти\",\n      \"sign_in_with_apihub\": \"Войти через APIHub\"\n    },\n    \"title\": {\n      \"logout\": \"Выйти\",\n      \"my_profile\": \"Мой профиль\"\n    }\n  },\n  \"button\": {\n    \"choose\": \"Выбрать\",\n    \"create\": \"Создать\",\n    \"edit\": \"Редактировать\",\n    \"delete\": \"Удалить\",\n    \"cancel\": \"Отмена\",\n    \"update\": \"Обновить\",\n    \"edit_profile\": \"Редактировать профиль\",\n    \"saving\": \"Сохранение...\",\n    \"save_changes\": \"Сохранить изменения\"\n  },\n  \"description\": {\n    \"label\": {\n      \"description\": \"Описание\"\n    },\n    \"title\": {\n      \"max_value\": \"Описание должно быть короче {{ quantity }} символов\"\n    }\n  },\n  \"playlists\": {\n    \"button\": {\n      \"choose_playlist\": \"Выбрать плейлист\",\n      \"create_playlist\": \"Создать плейлист\"\n    },\n    \"placeholder\": {\n      \"enter_playlist_description\": \"Введите описание плейлиста\",\n      \"enter_playlist_title\": \"Введите название плейлиста\",\n      \"search_playlist\": \"Поиск плейлиста\"\n    },\n    \"title\": {\n      \"all_playlists\": \"Все плейлисты\",\n      \"create_playlist\": \"Создать плейлист\",\n      \"edit_playlist\": \"Редактировать плейлист\",\n      \"new_playlists\": \"Новые плейлисты\",\n      \"playlists_not_found\": \"Плейлисты не найдены\"\n    },\n    \"label\": {\n      \"load_error\": \"Не удалось загрузить плейлист\"\n    },\n    \"aria_labels\": {\n      \"open_playlist\": \"Открыть плейлист {{title}}\"\n    }\n  },\n  \"sidebar\": {\n    \"all_playlists\": \"Все плейлисты\",\n    \"all_tracks\": \"Все треки\",\n    \"create_playlist\": \"Создать плейлист\",\n    \"home\": \"Главная\",\n    \"upload_track\": \"Загрузить трек\",\n    \"your_library\": \"Ваша библиотека\"\n  },\n  \"tabs\": {\n    \"playlists\": \"Плейлисты\",\n    \"tracks\": \"Треки\",\n    \"liked_playlists\": \"Любимые плейлисты\",\n    \"liked_tracks\": \"Любимые треки\",\n    \"possessive_case\": \"\"\n  },\n  \"tags\": {\n    \"label\": \"Хэштеги\",\n    \"placeholder\": \"Поиск по хэштегам\"\n  },\n  \"title\": {\n    \"max_value\": \"Название должно быть короче {{ quantity }} символов\",\n    \"min_value\": \"Название должно содержать не менее {{ quantity }} символов\",\n    \"required\": \"Название обязательно\",\n    \"title\": \"Название\"\n  },\n  \"tracks\": {\n    \"button\": {\n      \"add_to_playlist\": \"Добавить в плейлист\",\n      \"all_tracks\": \"Все треки\",\n      \"cancel\": \"Отмена\",\n      \"create\": \"Создать\",\n      \"edit\": \"Редактировать\",\n      \"delete\": \"Удалить\",\n      \"save\": \"Сохранить\",\n      \"show_text_song\": \"Показать текст песни\",\n      \"upload\": \"Загрузить\",\n      \"choose_track\": \"Выбрать трек\",\n      \"upload_track\": \"Загрузить трек\",\n      \"go_back\": \"Назад\",\n      \"delete_from_playlist\": \"Удалить из плейлиста\",\n      \"publish\": \"Опубликовать\",\n      \"draft\": \"Черновик\"\n    },\n    \"label\": {\n      \"lyrics\": \"Текст песни\",\n      \"title\": \"Название\",\n      \"no_tracks\": \"Нет треков\",\n      \"load_error\": \"Не удалось загрузить трек\"\n    },\n    \"placeholder\": {\n      \"lyrics\": \"Введите текст песни\",\n      \"search_tracks\": \"Поиск треков\",\n      \"title\": \"Введите название трека\",\n      \"no_lyrics\": \"Для данного трека нет текста\"\n    },\n    \"title\": {\n      \"all_tracks\": \"Все треки\",\n      \"create\": \"Создать трек\",\n      \"edit\": \"Редактировать трек\",\n      \"new_tracks\": \"Новые треки\",\n      \"tracks_not_found\": \"Треки не найдены\"\n    },\n    \"table\": {\n      \"track\": \"Трек\",\n      \"date_added\": \"Дата добавления\",\n      \"actions\": \"Действия\",\n      \"duration\": \"Длительность\"\n    },\n    \"release\": \"Дата релиза\"\n  },\n  \"date\": {\n    \"today\": \"сегодня\",\n    \"yesterday\": \"вчера\",\n    \"dayAgo\": \"{{addedAt}} день назад\",\n    \"fewDaysAgo\": \"{{addedAt}} дня назад\",\n    \"daysAgo\": \"{{addedAt}} дней назад\",\n    \"monthAgo\": \"{{addedAt}} месяц назад\",\n    \"fewMonthAgo\": \"{{addedAt}} месяца назад\",\n    \"monthsAgo\": \"{{addedAt}} месяцев назад\",\n    \"created\": \"Создан\"\n  },\n  \"sort\": {\n    \"label\": \"Сортировать по\",\n    \"newest_first\": \"Сначала новые\",\n    \"oldest_first\": \"Сначала старые\",\n    \"most_liked\": \"Самые популярные\",\n    \"least_liked\": \"Наименее популярные\"\n  },\n  \"placeholder\": {\n    \"upload_cover_image\": \"Загрузить обложку\",\n    \"search_and_select\": \"Найти и выбрать...\",\n    \"selected\": \"выбрано\",\n    \"no_options_found\": \"Ничего не найдено\",\n    \"all_options_selected\": \"Выбраны все\",\n    \"which_playlist\": \"В каком плейлисте находится трек?\"\n  },\n  \"profile\": {\n    \"title\": {\n      \"edit_profile\": \"Редактировать профиль\",\n      \"required_name\": \"Имя обязательно\",\n      \"required_surname\": \"Фамилия обязательна\",\n      \"max_value_name\": \"Имя должно быть меньше {{ quantity }} символов\",\n      \"min_value_name\": \"Имя должно содержать не менее {{ quantity }} символов\",\n      \"max_value_surname\": \"Фамилия должна быть меньше {{ quantity }} символов\",\n      \"min_value_surname\": \"Фамилия должна содержать не менее {{ quantity }} символов\"\n    },\n    \"placeholder\": {\n      \"enter_profile_name\": \"Введите имя профиля\",\n      \"enter_profile_surname\": \"Введите фамилию профиля\",\n      \"upload_avatar\": \"Загрузить аватар\"\n    },\n    \"label\": {\n      \"name\": \"Имя\",\n      \"surname\": \"Фамилия\"\n    },\n    \"stats\": {\n      \"playlists_one\": \"Плейлист\",\n      \"playlists_few\": \"Плейлиста\",\n      \"playlists_many\": \"Плейлистов\",\n      \"tracks_one\": \"Трек\",\n      \"tracks_few\": \"Трека\",\n      \"tracks_many\": \"Треков\"\n    }\n  },\n  \"playlist\": {\n    \"made_for\": \"Сделан для\",\n    \"tracks_count_one\": \"{{count}} трек\",\n    \"tracks_count_few\": \"{{count}} трека\",\n    \"tracks_count_many\": \"{{count}} треков\",\n    \"tracks_count_other\": \"{{count}} треков\"\n  },\n  \"player\": {\n    \"unknown_artist\": \"Неизвестный исполнитель\"\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/types/common.types.ts",
    "content": "export type Nullable<T> = T | null\n"
  },
  {
    "path": "apps/rtk-query/src/shared/types/commonApi.types.ts",
    "content": "export type ExtensionsError = {\n  data: {\n    extensions?: { key?: string; message?: string }[]\n  }\n}\n\nexport type Images = {\n  main: Cover[]\n}\n\nexport type Meta = {\n  page: number\n  pageSize: number\n  totalCount: number\n  pagesCount: number\n  nextCursor?: string // for cursor-based pagination\n  prevCursor?: string\n}\n\nexport type Cover = {\n  type: ImageType\n  width: number\n  height: number\n  fileSize: number\n  url: string\n}\n\nexport enum ImageType {\n  ORIGINAL = 'original',\n  MEDIUM = 'medium',\n  THUMBNAIL = 'thumbnail',\n}\n\nexport enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = -1,\n}\n\nexport type User = {\n  id: string\n  name: string\n}\n\nexport type ReactionResponse = {\n  objectId: string\n  value: number\n  likes: number\n  dislikes: number\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/types/index.ts",
    "content": "export * from './common.types.ts'\nexport * from './commonApi.types.ts'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/build-query-string.ts",
    "content": "type QueryParamValue = string | number | (string | number)[] | undefined | boolean\n\n/**\n * Формирует строку запроса (query string) из объекта параметров.\n *\n * Пропускает `undefined` и `null` значения.\n * Значения типа `number` автоматически преобразуются в строки.\n * Массивы сериализуются с повторяющимися ключами (например: `tagsIds=1&tagsIds=2`).\n *\n * @param {Record<string, QueryParamValue>} params - Объект с параметрами запроса\n * @returns {string} Готовая строка запроса (например: `key=value&arr=1&arr=2`)\n *\n * @example\n * buildQueryString({ search: 'text', tagsIds: [1, 2], page: 3 })\n * // Вернёт: \"search=text&tagsIds=1&tagsIds=2&page=3\"\n */\n\nexport function buildQueryString(params: Record<string, QueryParamValue>): string {\n  const searchParams = new URLSearchParams()\n  for (const key in params) {\n    const value = params[key]\n    if (value === undefined || value === null) continue\n\n    // Если это массив, добавляем каждый элемент отдельно (например: tagsIds=1&tagsIds=2)\n    if (Array.isArray(value)) {\n      value.forEach((val) => {\n        if (val !== undefined && val !== null) searchParams.append(key, String(val))\n      })\n    } else {\n      searchParams.append(key, String(value))\n    }\n  }\n  return searchParams.toString()\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/convert-file-to-base-64.ts",
    "content": "export const convertFileToBase64 = (file: File): Promise<string> =>\n  new Promise((resolve, reject) => {\n    const reader = new FileReader()\n    reader.onload = () => resolve(reader.result as string)\n    reader.onerror = reject\n    reader.readAsDataURL(file)\n  }) //! FIXME: temporary implementation until backend issue #160 is fixed\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/decode-file-from-base-64.ts",
    "content": "export const decodeFileFromBase64 = (data: string | null) => {\n  if (!data) return null\n\n  const mimeType = data.split(';')[0].split(':')[1]\n  const base64Url = data.split(',')[1]\n\n  const binaryString = atob(base64Url)\n  const binaryLength = binaryString.length\n  const bytes = new Uint8Array(binaryLength)\n\n  for (let i = 0; i < binaryLength; i++) {\n    bytes[i] = binaryString.charCodeAt(i)\n  }\n  const blob = new Blob([bytes], { type: mimeType })\n  return URL.createObjectURL(blob)\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/format-created-date.ts",
    "content": "import { getPluralKey } from '@/shared/utils/get-plural-key.ts'\n\nimport i18n from '../translations/i18nConfiguration.ts'\n\nexport const formatCreatedDate = (addedAt: string | undefined) => {\n  const lang = i18n.language || 'en'\n  if (!addedAt) {\n    return i18n.t('date.created')\n  }\n\n  const date = new Date(addedAt.toString())\n  const now = new Date()\n  const differTime = now.getTime() - date.getTime()\n  const differDays = Math.floor(differTime / (1000 * 60 * 60 * 24))\n  if (differDays === 0) {\n    return `${i18n.t('date.created')} ${i18n.t('date.today')}`\n  }\n  if (differDays < 30) {\n    const key = getPluralKey(differDays, lang, 'day')\n    const daysText = i18n.t(key, { addedAt: differDays })\n    return `${i18n.t('date.created')} ${daysText}`\n  }\n\n  const differMonths = Math.floor(differDays / 30)\n  if (differMonths < 12) {\n    const key = getPluralKey(differMonths, lang, 'month')\n    const monthsText = i18n.t(key, { addedAt: differMonths })\n    return `${i18n.t('date.created')} ${monthsText}`\n  }\n\n  const dateText = new Intl.DateTimeFormat(lang === 'ru' ? 'ru-RU' : 'en-GB', {\n    day: '2-digit',\n    month: '2-digit',\n    year: 'numeric',\n  }).format(date)\n\n  return `${i18n.t('date.created')} ${dateText}`\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/get-image-by-type.ts",
    "content": "import type { Images, ImageType } from '@/shared/types'\n\nexport const getImageByType = (images: Images, type: ImageType) => {\n  return images.main.find((image) => image.type === type)\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/get-plural-key.ts",
    "content": "import { getRussianPluralForm } from '@/shared/utils/get-russian-plural-form.ts'\n\nexport const getPluralKey = (count: number, lang: string, type: 'day' | 'month') => {\n  if (lang === 'en') {\n    return count === 1 ? `date.${type}Ago` : `date.${type}sAgo`\n  }\n\n  if (lang === 'ru') {\n    return getRussianPluralForm(count, type)\n  }\n  return `date.${type}sAgo`\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/get-russian-plural-form.ts",
    "content": "export const getRussianPluralForm = (count: number, type: 'day' | 'month'): string => {\n  const lastDigit = count % 10\n\n  if (lastDigit === 1 && count !== 11) {\n    return `date.${type}Ago`\n  }\n\n  if (lastDigit >= 2 && lastDigit <= 4 && !(count >= 12 && count <= 14)) {\n    return type === 'day' ? 'date.fewDaysAgo' : 'date.fewMonthAgo'\n  }\n\n  return `date.${type}sAgo`\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/get-user-initials.ts",
    "content": "import type { FullName } from '@/features/profile'\n\nexport const getUserInitials = (fullName?: FullName, login?: string) => {\n  return fullName?.name ? `${fullName.name[0]} ${fullName.surname[0]}` : (login?.[0] ?? '?')\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/index.ts",
    "content": "export * from './build-query-string'\nexport * from './convert-file-to-base-64'\nexport * from './decode-file-from-base-64'\nexport * from './get-image-by-type'\nexport * from './get-user-initials'\nexport * from './set-locale'\nexport * from './show-error-toast'\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/set-locale.ts",
    "content": "import i18n from 'i18next'\n\nimport { LOCALE_KEY } from '@/shared/constants'\n\nexport type Locale = 'en' | 'ru'\n\n/**\n * Switches application locale using i18next and persists it to localStorage.\n * Note: language change is async inside i18next, this function does not await it.\n *\n * @param {Locale} lng - Target locale code (e.g. \"en\" or \"ru\").\n * @returns {void}\n */\nexport const setLocale = (lng: Locale): void => {\n  void i18n.changeLanguage(lng)\n  localStorage.setItem(LOCALE_KEY, lng)\n}\n"
  },
  {
    "path": "apps/rtk-query/src/shared/utils/show-error-toast.ts",
    "content": "import { toast } from 'react-toastify'\n\nexport const showErrorToast = (message: string, error?: unknown) => {\n  toast(message, { theme: 'colored', type: 'error' })\n\n  if (error) {\n    console.error(`${message}\\n`, error)\n  }\n}\n"
  },
  {
    "path": "apps/rtk-query/src/styles/fonts.css",
    "content": "/*\n  source: https://gwfh.mranftl.com/fonts/lato?subsets=latin\n*/\n\n/* lato-regular - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../shared/assets/fonts/lato-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-700 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../shared/assets/fonts/lato-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-900 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 900;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../shared/assets/fonts/lato-v24-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n"
  },
  {
    "path": "apps/rtk-query/src/styles/global.css",
    "content": ":root {\n  font-family: Lato, sans-serif;\n  font-weight: 400;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  line-height: 100%;\n  text-rendering: optimizelegibility;\n\n  font-synthesis: none;\n}\n\n/* Scrollbar styles */\n* {\n  scrollbar-color: var(--color-bg-secondary) var(--color-bg-primary);\n  scrollbar-width: thin;\n}\n\nbody {\n  margin: 0;\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-primary);\n}\n"
  },
  {
    "path": "apps/rtk-query/src/styles/reset.css",
    "content": "/* Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset */\n\n/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Prevent font size inflation */\nhtml {\n  text-size-adjust: none;\n}\n\n/* Remove default margin in favour of better control in authored CSS */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nfigure,\nblockquote,\ndl,\ndd {\n  margin-block-end: 0;\n}\n\nul,\nol {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  line-height: 1.5;\n}\n\n/* Set shorter line heights on headings and interactive elements */\nh1,\nh2,\nh3,\nh4,\nbutton,\ninput,\nlabel {\n  border: none;\n  line-height: 1.1;\n}\n\n/* Balance text wrapping on headings */\nh1,\nh2,\nh3,\nh4 {\n  text-wrap: balance;\n}\n\n/* A elements that don't have a class get default styles */\na {\n  color: currentcolor;\n  text-decoration: none;\n}\n\n/* Make images easier to work with */\nimg,\npicture {\n  display: block;\n  max-width: 100%;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n/* Anything that has been anchored to should have extra scroll margin */\n:target {\n  scroll-margin-block: 5ex;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/styles/variables.css",
    "content": ":root {\n  /*\n  * Colors\n  */\n  --color-accent: #ff38b6;\n  --color-disabled: #858585;\n  --color-outline-focus: #1a75f5;\n\n  /* Text */\n  --color-text-primary: #fff;\n  --color-text-primary-reverse: #000;\n  --color-text-secondary: #b3b3b3;\n  --color-text-label: #808080;\n  --color-text-error: #f51a51;\n\n  /* Backgrounds */\n  --color-bg-primary: #000;\n  --color-bg-secondary: #141414;\n  --color-bg-primary-reverse: #fff;\n  --color-bg-input-hover: #262626;\n  --color-bg-card: rgb(7 7 7 / 50%);\n  --color-bg-interactive-secondary: #333;\n\n  /* Borders */\n  --color-border-base: #7f7f7f;\n  --color-border-input-primary: #4d4d4d;\n  --color-border-input-active: #fffefe;\n\n  /*\n  * Typography\n  */\n\n  /* font-sizes */\n  --font-size-xxxs: 12px;\n  --font-size-xxs: 13px;\n  --font-size-xs: 14px;\n  --font-size-s: 16px;\n  --font-size-m: 18px;\n  --font-size-l: 20px;\n  --font-size-xl: 24px;\n  --font-size-xxl: 30px;\n  --font-size-xxxl: 60px;\n  --font-size-xxxxl: 80px;\n\n  /*\n  * Layout\n  */\n  --header-height: 80px;\n  --player-height: 112px;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/rtk-query/src/widgets/Player/Player.module.css",
    "content": ".player {\n  grid-area: player;\n  text-align: center;\n}\n"
  },
  {
    "path": "apps/rtk-query/src/widgets/Player/Player.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { useFetchTracksQuery } from '@/features/tracks'\nimport {\n  useCurrentTrack,\n  usePlaybackModes,\n  usePlaybackProgress,\n  usePlaybackState,\n  usePlayerControls,\n  useVolumeControl,\n} from '@/player'\nimport {\n  convertApiTracksToPlayerTracks,\n  convertApiTrackToPlayerTrack,\n} from '@/player/utils/convert-api-track-to-player-track.ts'\nimport noCoverPlaceholder from '@/shared/assets/images/no-cover-placeholder.avif'\nimport { AudioPlayer } from '@/shared/components'\nimport { AudioPlayerSkeleton } from '@/shared/components/AudioPlayer/AudioPlayerSceleton/AudioPlayerSkeleton.tsx'\n\nimport s from './Player.module.css'\n\nexport const Player = () => {\n  const { t } = useTranslation()\n  const { track: currentTrack } = useCurrentTrack()\n  const { shuffleMode, repeatMode, setRepeatMode, toggleShuffle } = usePlaybackModes()\n  const { isPlaying } = usePlaybackState()\n  const { seek, pause, resume, next, previous, play } = usePlayerControls()\n  const { currentTime, duration } = usePlaybackProgress()\n  const { volume, setVolume } = useVolumeControl()\n\n  const { data: tracks, isLoading: isApiTracksLoading } = useFetchTracksQuery({\n    pageSize: 10,\n    pageNumber: 1,\n  })\n\n  const firstTrack = tracks?.data[0]\n  const playerTrack = firstTrack ? convertApiTrackToPlayerTrack(firstTrack) : null\n  const allPlayerTracks = tracks?.data ? convertApiTracksToPlayerTracks(tracks.data) : []\n  const cover = firstTrack?.attributes.images.main[1]?.url // if you use 0 - image is blurred, if you use 1 - image is clear\n  const title = firstTrack?.attributes.title\n  const artistName = playerTrack?.artist || t('player.unknown_artist')\n\n  const handleNextTrack = () => {\n    next()\n  }\n  const handlePreviousTrack = () => {\n    previous()\n  }\n  const handleTogglePlay = () => {\n    if (currentTrack) {\n      return isPlaying ? pause() : resume()\n    }\n\n    if (firstTrack && allPlayerTracks.length > 0) {\n      return play(playerTrack!, undefined, allPlayerTracks)\n    }\n\n    return undefined\n  }\n  const handleToggleShuffle = () => {\n    toggleShuffle()\n  }\n  const handleSetRepeatMode = () => {\n    setRepeatMode()\n  }\n\n  return isApiTracksLoading ? (\n    <AudioPlayerSkeleton />\n  ) : (\n    <AudioPlayer\n      cover={currentTrack?.albumArt || (currentTrack ? noCoverPlaceholder : cover!)}\n      title={currentTrack?.title || title!}\n      artist={currentTrack?.artist || artistName}\n      isPlaying={isPlaying}\n      onNext={handleNextTrack}\n      onPrevious={handlePreviousTrack}\n      onTogglePlay={handleTogglePlay}\n      isShuffle={shuffleMode}\n      isRepeat={repeatMode}\n      onShuffle={handleToggleShuffle}\n      onRepeat={handleSetRepeatMode}\n      className={s.player}\n      duration={duration}\n      currentTime={currentTime}\n      volume={volume}\n      onTimeSeek={seek}\n      onVolumeSet={setVolume}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/rtk-query/src/widgets/Player/index.ts",
    "content": "export * from './Player'\n"
  },
  {
    "path": "apps/rtk-query/stylelint.config.js",
    "content": "export default {\n  extends: ['stylelint-config-standard', 'stylelint-config-clean-order'],\n  rules: {\n    // Class selector pattern (allow camelCase for CSS modules)\n    'selector-class-pattern': null,\n\n    // Allow unknown at-rules (for CSS modules :global, :local etc)\n    'at-rule-no-unknown': [\n      true,\n      {\n        ignoreAtRules: ['global', 'local'],\n      },\n    ],\n  },\n\n  // File patterns to lint\n  ignoreFiles: ['dist/**/*', 'build/**/*', 'node_modules/**/*'],\n}\n"
  },
  {
    "path": "apps/rtk-query/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "apps/rtk-query/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "apps/rtk-query/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "apps/rtk-query/vite.config.ts",
    "content": "import path from 'node:path'\n\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  base: '/rtkquery',\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, 'src'),\n    },\n  },\n  server: {\n    host: true, // ← or '0.0.0.0'\n    port: 5176,\n    strictPort: true,\n    allowedHosts: [\n      'domain.prod', // <-- your custom host\n      'localhost', // (optional) keep localhost too\n    ],\n  },\n})\n"
  },
  {
    "path": "apps/tanstack-query-zustand/.claude/skills/i18n-rules/SKILL.md",
    "content": "---\nname: i18n-rules\ndescription: Rules for adding internationalization (i18n) translations. Activated automatically when adding new UI text, buttons, labels, or any user-facing strings. Use when user says \"add translation\", \"добавь перевод\", \"i18n\", \"интернационализация\".\nautoActivate: always\n---\n\n# Skill: i18n Rules\n\n## Purpose\n\nEnsure every new user-facing string is properly internationalized in **both** language files.\n\n## Translation Files\n\n| Language | File                                        |\n| -------- | ------------------------------------------- |\n| English  | `src/shared/translations/languages/en.json` |\n| Russian  | `src/shared/translations/languages/ru.json` |\n\n## Rules\n\n1. **Always add to both files.** Every new key must be added to `en.json` AND `ru.json` simultaneously.\n\n2. **Key structure.** Keys are nested by domain and category using dot notation:\n\n   - `<domain>.<category>.<key>` — e.g. `tracks.button.publish`, `playlists.title.create_playlist`\n   - Top-level domains: `auth`, `tracks`, `playlists`, `sidebar`, `tabs`, `tags`, `title`, `description`, `sort`, `placeholder`, `profile`, `date`, `playlist`, `player`, `common`, `button`, `image_uploader`, `artists`\n   - Common categories within a domain: `button`, `label`, `title`, `placeholder`, `error`, `success`, `table`, `aria_labels`, `stats`\n\n3. **Key naming.** Use `snake_case` for all keys. Keep names short and descriptive: `delete_from_playlist`, `show_text_song`, `upload_track`.\n\n4. **Placement.** Add new keys next to related existing keys within the same domain/category block. Maintain alphabetical order when practical, but grouping by feature context takes priority.\n\n5. **Interpolation.** Use `{{ variable }}` syntax (with spaces inside braces) for dynamic values:\n\n   - `\"max_value\": \"Title must be less than {{ quantity }} characters\"`\n   - `\"file_too_large\": \"The file is too large. Max size is {{size}} MB\"`\n\n6. **Plurals (Russian).** Russian requires `_one`, `_few`, `_many` suffixes. English uses `_one`, `_other`:\n\n   - EN: `\"tracks_count_one\": \"{{count}} track\"`, `\"tracks_count_other\": \"{{count}} tracks\"`\n   - RU: `\"tracks_count_one\": \"{{count}} трек\"`, `\"tracks_count_few\": \"{{count}} трека\"`, `\"tracks_count_many\": \"{{count}} треков\"`\n\n7. **Usage in components.** Import `useTranslation` from `react-i18next` and use the `t()` function:\n\n   ```tsx\n   const { t } = useTranslation()\n   // ...\n   {\n     t('tracks.button.publish')\n   }\n   ```\n\n8. **JSON validity.** Ensure trailing commas are correct — the last key in a block must NOT have a trailing comma. Always verify both files remain valid JSON after editing.\n\n9. **No hardcoded strings.** Never leave user-facing text as raw strings in components. Always use `t('...')`.\n\n10. **Russian translations must be real.** Do not use transliteration or machine-translated gibberish. Provide natural, idiomatic Russian text.\n"
  },
  {
    "path": "apps/tanstack-query-zustand/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.cursor\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n*storybook.log\nstorybook-static\n\n.env.development"
  },
  {
    "path": "apps/tanstack-query-zustand/.storybook/main.ts",
    "content": "import type { StorybookConfig } from '@storybook/react-vite'\n\nconst config: StorybookConfig = {\n  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],\n  addons: [],\n  framework: {\n    name: '@storybook/react-vite',\n    options: {},\n  },\n}\nexport default config\n"
  },
  {
    "path": "apps/tanstack-query-zustand/.storybook/preview.tsx",
    "content": "import '../src/app/styles/fonts.css'\nimport '../src/app/styles/variables.css'\nimport '../src/app/styles/reset.css'\nimport '../src/app/styles/global.css'\n\nimport type { Preview } from '@storybook/react-vite'\nimport React from 'react'\nimport { BrowserRouter } from 'react-router'\n\nconst preview: Preview = {\n  parameters: {\n    controls: {\n      matchers: {\n        color: /(background|color)$/i,\n        date: /Date$/i,\n      },\n    },\n  },\n  decorators: [\n    (Story) => (\n      <BrowserRouter>\n        <Story />\n      </BrowserRouter>\n    ),\n  ],\n}\n\nexport default preview\n"
  },
  {
    "path": "apps/tanstack-query-zustand/AGENTS.md",
    "content": "# MusicFun - TanStack Query + Zustand Stack\n\n## Stack\n\n| Category        | Technology                                           | Version |\n| --------------- | ---------------------------------------------------- | ------- |\n| Framework       | React                                                | 19.1.0  |\n| Routing         | React Router                                         | 7.6.2   |\n| Server State    | TanStack Query (@tanstack/react-query)               | -       |\n| Client State    | Zustand                                              | -       |\n| Build Tool      | Vite                                                 | -       |\n| Language        | TypeScript                                           | ~5.8.3  |\n| UI Components   | @headlessui/react                                    | -       |\n| Forms           | react-hook-form + zod                                | -       |\n| i18n            | i18next + react-i18next                              | -       |\n| Infinite Scroll | react-intersection-observer                          | -       |\n| API Types       | OpenAPI-generated (schema.ts via openapi-typescript) | -       |\n| API Client      | openapi-fetch                                        | 0.14.0  |\n| Storybook       | Storybook                                            | -       |\n| CSS             | CSS Modules                                          | -       |\n| Linting         | ESLint + Stylelint + Prettier                        | -       |\n\n## Config\n\n- **Base URL path**: `/tanstack-zustand`\n- **Dev server port**: 5175\n- **API Base**: `import.meta.env.VITE_BASE_URL`\n- **API Key**: `import.meta.env.VITE_API_KEY`\n- **Auth Token fallback**: `import.meta.env.VITE_AUTH_TOKEN`\n\n## Architecture\n\n**Pattern**: Feature-Sliced Design (FSD), аналогичен RTK Query проекту, с отличиями в слое entities и подходе к API.\n\n```\nsrc/\n├── app/                        # Application layer\n│   └── routing/                # React Router routes definition\n├── entities/                   # Entity layer (FSD)\n│   └── playlist/               # PlaylistCard, PlaylistItem entities\n├── features/                   # Feature modules\n│   ├── auth/                   # Auth: login modal, OAuth\n│   │   ├── api/                # TanStack Query hooks (useMeQuery, useLogin, useLogout)\n│   │   ├── model/              # Нет Redux slice, используется Zustand / локальный стейт\n│   │   └── ui/                 # LoginModal, AccountMenu\n│   ├── playlists/              # Playlists feature\n│   │   ├── api/                # TanStack Query hooks (usePlaylists, usePlaylist) + mocks\n│   │   ├── model/              # Zustand store (playlists-store: модалки create/edit)\n│   │   └── ui/                 # PlaylistCard, PlaylistActions\n│   ├── tracks/                 # Tracks feature\n│   │   ├── api/                # TanStack Query hooks + mocks\n│   │   └── ui/                 # TrackCard, TrackRow, TrackRowContainer, TracksTable\n│   ├── tags/                   # Tags feature\n│   │   ├── api/                # TanStack Query hooks (useTags)\n│   │   └── ui/                 # TagsList\n│   └── artists/                # Artists feature\n│       └── api/                # Mock data (MOCK_ARTISTS)\n├── pages/                      # Page-level components\n│   ├── MainPage/\n│   ├── TracksPage/\n│   │   └── model/              # useTracksInfinityQuery hook\n│   ├── PlaylistsPage/\n│   ├── TrackPage/\n│   ├── PlaylistPage/\n│   ├── UserPage/\n│   │   └── ui/                 # UserInfo, UserTabs (Playlists, Tracks, LikedPlaylists, LikedTracks)\n│   ├── auth/                   # OAuthCallback page\n│   └── common/                 # Shared page components\n│       └── ContentList, PageWrapper, SearchTextField, SortSelect\n├── layout/                     # App shell\n│   ├── Header/\n│   ├── Sidebar/\n│   └── Layout.tsx\n├── widgets/                    # Complex UI widgets\n│   └── Player/                 # Music player widget UI\n├── player/                     # Player business logic\n│   ├── model/\n│   │   ├── player-store.ts     # Zustand store (playback, queue, modes)\n│   │   └── audio-manager.ts    # AudioManager singleton class\n│   ├── types/                  # Player types\n│   └── utils/                  # Track conversion utils\n└── shared/                     # Shared layer\n    ├── api/                    # API client setup, schema.ts (OpenAPI types)\n    ├── components/             # UI Kit (Button, Card, Skeleton, Tabs, Autocomplete, etc.)\n    ├── hooks/                  # useDebounceValue, etc.\n    ├── types/                  # Common types\n    ├── utils/                  # authStorage, VU (validation utils), etc.\n    └── icons/                  # SVG icon components\n```\n\n## State Management\n\n### Server State (TanStack Query)\n\n- Custom hooks на базе `useQuery` / `useInfiniteQuery` / `useMutation`\n- Типы из `schema.ts` (OpenAPI-generated, см. раздел \"API Schema\")\n- Query keys по конвенции: `['tracks', params]`, `['playlists', params]`\n- Нет системы тегов/инвалидации как в RTK Query (ручной `invalidateQueries`)\n\n### Client State (Zustand)\n\n| Store             | Назначение                                               |\n| ----------------- | -------------------------------------------------------- |\n| `player-store`    | Полное состояние плеера (playback, queue, volume, modes) |\n| `playlists-store` | Состояние модалки создания/редактирования плейлиста      |\n\n### Auth Flow\n\n1. OAuth login -> tokens через `authStorage` utility\n2. `authStorage.getAccessToken()` / `saveAccessToken()` / `clearTokens()`\n3. Хранение в localStorage как JSON: `{ accessToken: \"...\" }`\n4. Keys: `musicfun-access-token`, `musicfun-refresh-token`\n\n## Player Architecture\n\n- **Store**: Zustand `player-store.ts`\n- **Audio**: `AudioManager` singleton class (not raw `new Audio()`)\n  - Event system (on/off/emit) с типизированными событиями\n  - Throttled timeupdate (0.5s interval)\n  - loadTrack с timeout (30s) и Promise-based API\n  - Методы: play, pause, stop, seek, setVolume, setMuted\n- **State**: playbackState, currentTrack, queue, volume, repeatMode, shuffleMode\n- **Hooks**: `usePlayerControls`, `useCurrentTrack`, `usePlaybackProgress`, `usePlaybackState`\n\n## Routes\n\n| Route              | Page          | Description                     |\n| ------------------ | ------------- | ------------------------------- |\n| `/`                | MainPage      | Tags, new playlists, new tracks |\n| `/tracks`          | TracksPage    | All tracks with infinite scroll |\n| `/tracks/:id`      | TrackPage     | Single track detail             |\n| `/playlists`       | PlaylistsPage | All playlists with pagination   |\n| `/playlists/:id`   | PlaylistPage  | Playlist detail with tracks     |\n| `/profile/:userId` | UserPage      | User profile with tabs          |\n| `/oauth/callback`  | OAuthCallback | OAuth redirect handler          |\n\n## Commands\n\n```bash\npnpm dev          # Start dev server (port 5175)\npnpm build        # Build for production\npnpm storybook    # Start Storybook\n```\n\n## API Schema\n\n**ВАЖНО: `src/shared/api/schema.ts` - автогенерируемый файл. НИКОГДА не редактировать вручную.**\n\nГенерация типов из OpenAPI-спецификации бэкенда:\n\n```bash\npnpm generate:api    # Генерирует schema.ts из https://musicfun.it-incubator.app/api-json\n```\n\n- Используется `openapi-typescript` для генерации TypeScript типов\n- Используется `openapi-fetch` как типизированный HTTP-клиент\n- Флаги генерации: `--root-types --enum --enum-values --dedupe-enums`\n- При изменении API бэкенда: перегенерировать через `pnpm generate:api`\n"
  },
  {
    "path": "apps/tanstack-query-zustand/CLAUDE.md",
    "content": "apps/tanstack-query-zustand/AGENTS.md\n"
  },
  {
    "path": "apps/tanstack-query-zustand/README.md",
    "content": "UI for Musicfun app without libs\n\nTODO:\n[] Add common components for TrackOverview and PlaylistOverview\n[] Refactor DropdownMenu\n"
  },
  {
    "path": "apps/tanstack-query-zustand/eslint.config.js",
    "content": "// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format\nimport js from '@eslint/js'\nimport prettier from 'eslint-config-prettier'\nimport eslintPluginPrettier from 'eslint-plugin-prettier'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport simpleImportSort from 'eslint-plugin-simple-import-sort'\nimport storybook from 'eslint-plugin-storybook'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default [\n  { ignores: ['dist'] },\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n    plugins: {\n      'react-hooks': reactHooks,\n      'react-refresh': reactRefresh,\n      prettier: eslintPluginPrettier,\n      'simple-import-sort': simpleImportSort,\n    },\n    rules: {\n      ...reactHooks.configs.recommended.rules,\n      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],\n      'prettier/prettier': 'warn',\n      'simple-import-sort/imports': 'error',\n      'simple-import-sort/exports': 'error',\n    },\n  },\n  ...storybook.configs['flat/recommended'],\n]\n"
  },
  {
    "path": "apps/tanstack-query-zustand/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Musicfun</title>\n    <!-- SPA redirect handler for GitHub Pages -->\n    <script>\n      ;(function () {\n        var redirect = sessionStorage.redirect\n        delete sessionStorage.redirect\n        if (redirect && redirect !== location.href) {\n          history.replaceState(null, null, redirect)\n        }\n\n        // Check for spa_redirect query parameter\n        var searchParams = new URLSearchParams(window.location.search)\n        var spaRedirect = searchParams.get('spa_redirect')\n        if (spaRedirect) {\n          searchParams.delete('spa_redirect')\n          var newSearch = searchParams.toString()\n          var newUrl = decodeURIComponent(spaRedirect)\n          sessionStorage.redirect = newUrl\n          window.location.replace(window.location.pathname + (newSearch ? '?' + newSearch : ''))\n        }\n      })()\n    </script>\n  </head>\n  <body>\n    <div id=\"root\">\n      <svg\n        width=\"40\"\n        height=\"40\"\n        viewBox=\"0 0 40 40\"\n        style=\"position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)\">\n        <style>\n          @keyframes bounce {\n            0%,\n            100% {\n              transform: scaleY(0.3);\n            }\n            50% {\n              transform: scaleY(1);\n            }\n          }\n          .bar {\n            animation: bounce 0.8s ease-in-out infinite;\n            transform-origin: bottom;\n          }\n          .bar:nth-child(2) {\n            animation-delay: 0.1s;\n          }\n          .bar:nth-child(3) {\n            animation-delay: 0.2s;\n          }\n          .bar:nth-child(4) {\n            animation-delay: 0.3s;\n          }\n        </style>\n        <rect class=\"bar\" x=\"4\" y=\"10\" width=\"6\" height=\"20\" rx=\"3\" fill=\"#8b5cf6\" />\n        <rect class=\"bar\" x=\"13\" y=\"5\" width=\"6\" height=\"30\" rx=\"3\" fill=\"#a78bfa\" />\n        <rect class=\"bar\" x=\"22\" y=\"8\" width=\"6\" height=\"24\" rx=\"3\" fill=\"#c4b5fd\" />\n        <rect class=\"bar\" x=\"31\" y=\"12\" width=\"6\" height=\"16\" rx=\"3\" fill=\"#ddd6fe\" />\n      </svg>\n    </div>\n    <script type=\"module\" src=\"/src/app/entrypoint/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/tanstack-query-zustand/package.json",
    "content": "{\n  \"name\": \"tanstack-query-zustand\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"type-check\": \"tsc -b\",\n    \"test\": \"vitest\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"lint:css\": \"stylelint \\\"src/**/*.{css,scss}\\\"\",\n    \"lint:css:fix\": \"stylelint \\\"src/**/*.{css,scss}\\\" --fix\",\n    \"format\": \"prettier --write .\",\n    \"preview\": \"vite preview\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"build-storybook\": \"storybook build\",\n    \"generate:api\": \"pnpm openapi-typescript https://musicfun.it-incubator.app/api-json -o ./src/shared/api/schema.ts --root-types --enum --enum-values --dedupe-enums\",\n    \"generate:api:dimych\": \"pnpm openapi-typescript http://localhost:9001/api-json -o ./src/shared/api/schema.ts --root-types\"\n  },\n  \"dependencies\": {\n    \"@tanstack/react-query\": \"^5.81.5\",\n    \"@tanstack/react-query-devtools\": \"^5.81.5\",\n    \"i18next\": \"^25.7.4\",\n    \"i18next-browser-languagedetector\": \"^8.2.0\",\n    \"openapi-fetch\": \"^0.14.0\",\n    \"react\": \"19.2.0\",\n    \"react-dom\": \"19.2.0\",\n    \"react-hook-form\": \"^7.60.0\",\n    \"react-i18next\": \"^16.5.1\",\n    \"react-intersection-observer\": \"^10.0.0\",\n    \"react-router\": \"7.6.2\",\n    \"react-toastify\": \"^11.0.5\",\n    \"zustand\": \"^5.0.8\"\n  },\n  \"devDependencies\": {\n    \"@storybook/react-vite\": \"9.0.8\",\n    \"@types/node\": \"^24.0.1\",\n    \"@types/react\": \"^19.1.2\",\n    \"@types/react-dom\": \"^19.1.2\",\n    \"@vitejs/plugin-react\": \"^4.4.1\",\n    \"clsx\": \"^2.1.1\",\n    \"openapi-typescript\": \"^7.8.0\",\n    \"sass-embedded\": \"^1.93.3\",\n    \"storybook\": \"9.0.8\",\n    \"stylelint\": \"^16.20.0\",\n    \"stylelint-config-clean-order\": \"^7.0.0\",\n    \"stylelint-config-standard\": \"^38.0.0\",\n    \"typescript\": \"~5.8.3\",\n    \"vite\": \"^6.3.5\",\n    \"vitest\": \"^4.0.10\"\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/App.css",
    "content": "#root {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n  filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  a:nth-of-type(2) .logo {\n    animation: logo-spin infinite 20s linear;\n  }\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/App.tsx",
    "content": "import { useState } from 'react'\nimport reactLogo from './assets/react.svg'\nimport viteLogo from '/vite.svg'\nimport './App.css'\n\nfunction App() {\n  const [count, setCount] = useState(0)\n\n  return (\n    <>\n      <div>\n        <a href=\"https://vite.dev\" target=\"_blank\">\n          <img src={viteLogo} className=\"logo\" alt=\"Vite logo\" />\n        </a>\n        <a href=\"https://react.dev\" target=\"_blank\">\n          <img src={reactLogo} className=\"logo react\" alt=\"React logo\" />\n        </a>\n      </div>\n      <h1>Vite + React</h1>\n      <div className=\"card\">\n        <button onClick={() => setCount((count) => count + 1)}>count is {count}</button>\n        <p>\n          Edit <code>src/App.tsx</code> and save to test HMR\n        </p>\n      </div>\n      <p className=\"read-the-docs\">Click on the Vite and React logos to learn more</p>\n    </>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/App.tsx",
    "content": "import '@/shared/translations/i18nConfiguration'\nimport { ToastContainer } from 'react-toastify'\n\nimport { Routing } from './routing'\n\nexport const App = () => {\n  return (\n    <>\n      <Routing />\n      <ToastContainer />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/entrypoint/main.tsx",
    "content": "import '@/app/styles/fonts.css'\nimport '@/app/styles/variables.css'\nimport '@/app/styles/reset.css'\nimport '@/app/styles/global.css'\nimport 'react-toastify/dist/ReactToastify.css'\n\nimport { QueryClientProvider } from '@tanstack/react-query'\nimport { createRoot } from 'react-dom/client'\nimport { BrowserRouter } from 'react-router'\nimport { toast } from 'react-toastify'\n\nimport { queryClient } from '@/app/query-client/query-client.tsx'\nimport { setClientConfig } from '@/shared/api/client.ts'\nimport { API_BASE_URL, API_KEY, CURRENT_APP_DOMAIN } from '@/shared/config/config.ts'\nimport { PrerenderReady } from '@/shared/ui/prerender-ready.tsx'\nimport { authStorage } from '@/shared/utils/authStorage.ts'\nimport { initializePlayer } from '@/player/model/player-store.ts'\n\nimport { App } from '../App.tsx'\n\n// Initialize player audio listeners\ninitializePlayer()\n\nexport type MutationMeta = {\n  /**\n   * Если 'off' — глобальный обработчик ошибок пропускаем,\n   * если 'on' (или нет поля) — вызываем.\n   */\n  globalErrorHandler?: 'on' | 'off'\n}\n\ndeclare module '@tanstack/react-query' {\n  interface Register {\n    /**\n     * Тип для поля `meta` в useMutation(...)\n     */\n    mutationMeta: MutationMeta\n  }\n}\n\nsetClientConfig({\n  baseURL: API_BASE_URL,\n  apiKey: API_KEY,\n  getAccessToken: async () => authStorage.getAccessToken(),\n  getRefreshToken: async () => authStorage.getRefreshToken(),\n  saveAccessToken: async (token) =>\n    token ? authStorage.saveAccessToken(token) : authStorage.clearAccessToken(),\n  saveRefreshToken: async (token) =>\n    token ? authStorage.saveRefreshToken(token) : authStorage.clearRefreshToken(),\n\n  toManyRequestsErrorHandler: (message: string | null) => {\n    toast(message)\n  },\n  logoutHandler: () => {\n    // store.dispatch(logoutThunk())\n  },\n})\n\nconst baseName = CURRENT_APP_DOMAIN ? '/' + CURRENT_APP_DOMAIN : ''\n\ncreateRoot(document.getElementById('root')!).render(\n  <QueryClientProvider client={queryClient}>\n    <BrowserRouter basename={baseName}>\n      <App />\n      <PrerenderReady />\n    </BrowserRouter>\n  </QueryClientProvider>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/query-client/query-client.tsx",
    "content": "import { MutationCache, QueryClient } from '@tanstack/react-query'\n\nimport { mutationGlobalErrorHandler } from '@/shared/ui/utils/query-error-handler-for-rhf-factory.ts'\n\nexport const queryClient = new QueryClient({\n  mutationCache: new MutationCache({\n    onError: mutationGlobalErrorHandler, // 🔹 вызывается ВСЕГДА\n  }),\n  defaultOptions: {\n    queries: {\n      refetchOnWindowFocus: false,\n      refetchOnMount: false,\n      staleTime: Infinity, //5000,\n      //gcTime: 10000 // если нет подписчиков - удалить всё нафик...\n    },\n  },\n})\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/routing/Routing.tsx",
    "content": "import { Route, Routes } from 'react-router'\n\nimport { Layout } from '@/layout'\nimport {\n  MainPage,\n  PlaylistPage,\n  PlaylistsPage,\n  TrackLyricsPage,\n  TrackPage,\n  TracksPage,\n  UserPage,\n} from '@/pages'\nimport { OAuthCallback } from '@/pages/auth/OAuthRedirect/OAuthCallback.tsx'\n\nexport const Routing = () => (\n  <Routes>\n    <Route path=\"/oauth/callback\" element={<OAuthCallback />} />\n    <Route path=\"/\" element={<Layout />}>\n      <Route index element={<MainPage />} />\n\n      <Route path=\"/tracks\" element={<TracksPage />} />\n      <Route path=\"/tracks/:id\" element={<TrackPage />} />\n      <Route path=\"/tracks/:id/lyrics\" element={<TrackLyricsPage />} />\n\n      <Route path=\"/playlists\" element={<PlaylistsPage />} />\n      <Route path=\"/playlists/:id\" element={<PlaylistPage />} />\n\n      <Route path=\"/user/:id\" element={<UserPage />} />\n    </Route>\n  </Routes>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/routing/index.ts",
    "content": "export { Routing } from './Routing'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/styles/fonts.css",
    "content": "/*\n  source: https://gwfh.mranftl.com/fonts/lato?subsets=latin\n*/\n\n/* lato-regular - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-700 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-900 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 900;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../../shared/fonts/lato-v24-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/styles/global.css",
    "content": ":root {\n  font-family: Lato, sans-serif;\n  font-weight: 400;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  line-height: 100%;\n  text-rendering: optimizelegibility;\n\n  font-synthesis: none;\n}\n\n/* Scrollbar styles */\n* {\n  scrollbar-color: var(--color-bg-secondary) var(--color-bg-primary);\n  scrollbar-width: thin;\n}\n\nbody {\n  margin: 0;\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-primary);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/styles/reset.css",
    "content": "/* Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset */\n\n/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Prevent font size inflation */\nhtml {\n  text-size-adjust: none;\n}\n\n/* Remove default margin in favour of better control in authored CSS */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nfigure,\nblockquote,\ndl,\ndd {\n  margin-block-end: 0;\n}\n\nul,\nol {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  line-height: 1.5;\n}\n\n/* Set shorter line heights on headings and interactive elements */\nh1,\nh2,\nh3,\nh4,\nbutton,\ninput,\nlabel {\n  border: none;\n  line-height: 1.1;\n}\n\n/* Balance text wrapping on headings */\nh1,\nh2,\nh3,\nh4 {\n  text-wrap: balance;\n}\n\n/* A elements that don't have a class get default styles */\na {\n  color: currentcolor;\n  text-decoration: none;\n}\n\n/* Make images easier to work with */\nimg,\npicture {\n  display: block;\n  max-width: 100%;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n/* Anything that has been anchored to should have extra scroll margin */\n:target {\n  scroll-margin-block: 5ex;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/app/styles/variables.css",
    "content": ":root {\n  /*\n  * Colors\n  */\n  --color-accent: #ff38b6;\n  --color-disabled: #858585;\n  --color-outline-focus: #1a75f5;\n\n  /* Text */\n  --color-text-primary: #fff;\n  --color-text-primary-reverse: #000;\n  --color-text-secondary: #b3b3b3;\n  --color-text-label: #808080;\n  --color-text-error: #f51a51;\n\n  /* Backgrounds */\n  --color-bg-primary: #000;\n  --color-bg-secondary: #141414;\n  --color-bg-primary-reverse: #fff;\n  --color-bg-input-hover: #262626;\n  --color-bg-card: rgb(7 7 7 / 50%);\n  --color-bg-interactive-secondary: #333;\n\n  /* Borders */\n  --color-border-base: #7f7f7f;\n  --color-border-input-primary: #4d4d4d;\n  --color-border-input-active: #fffefe;\n\n  /*\n  * Typography\n  */\n\n  /* font-sizes */\n  --font-size-xxxs: 12px;\n  --font-size-xxs: 13px;\n  --font-size-xs: 14px;\n  --font-size-s: 16px;\n  --font-size-m: 18px;\n  --font-size-l: 20px;\n  --font-size-xl: 24px;\n  --font-size-xxl: 30px;\n  --font-size-xxxl: 60px;\n\n  /*\n  * Layout\n  */\n  --header-height: 80px;\n  --player-height: 112px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/index.tsx",
    "content": "export { PlaylistCard } from './ui/PlaylistCard'\nexport { PlaylistCardSkeleton } from './ui/PlaylistCardSkeleton'\nexport { PlaylistItem } from './ui/PlaylistItem'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistCard/PlaylistCard.module.scss",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n\n  width: 264px;\n  min-height: 225px;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.8;\n}\n\n.image {\n  overflow: hidden;\n  height: 153px;\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n\n  img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n}\n\n.titleWrapper {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.menuIcon {\n  cursor: pointer;\n  width: 16px;\n  height: 16px;\n}\n\n.deleteItem {\n  color: var(--color-text-error);\n\n  &:hover {\n    color: var(--color-text-secondary);\n  }\n}\n\n.description {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.withReactionButtons {\n  min-height: 267px;\n}\n\n.reactionButtons {\n  margin-top: auto;\n}\n\n.details {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.detailsRow {\n  display: flex;\n  gap: 4px;\n  min-width: 0;\n}\n\n.dot::before {\n  content: '•';\n  margin: 0 4px;\n  font-size: 17px;\n  opacity: 0.7;\n}\n\n.madeFor {\n  overflow: hidden;\n  display: flex;\n  gap: 4px;\n  align-items: baseline;\n\n  width: 100%;\n  min-width: 0;\n}\n\n.userLink {\n  cursor: pointer;\n\n  overflow: hidden;\n  display: inline-block;\n  flex-shrink: 1;\n\n  min-width: 0;\n  max-width: 100%;\n\n  line-height: inherit;\n  color: white;\n  text-decoration: underline;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n\n  background: none;\n\n  transition: transform 0.4s;\n\n  &:hover {\n    transform: scale(1.02);\n  }\n\n  &:active {\n    transform: scale(1.005);\n  }\n}\n\n.tracks {\n  flex-shrink: 0;\n}\n\n.created {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.madeForText {\n  flex-shrink: 0;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistCard/PlaylistCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ImageSizeType } from '@/shared/api/schema.ts'\n\nimport { PlaylistCard } from './PlaylistCard.tsx'\n\nconst meta: Meta<typeof PlaylistCard> = {\n  title: 'entities/PlaylistCard',\n  component: PlaylistCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistCard>\n\nexport const Default: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    images: {\n      main: [\n        {\n          url: 'https://unsplash.it/182/182',\n          fileSize: 12,\n          width: 1212,\n          height: 2323,\n          type: ImageSizeType.medium,\n        },\n      ],\n    },\n  },\n}\n\nexport const WithReactions: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    images: {\n      main: [\n        {\n          url: 'https://unsplash.it/182/182',\n          fileSize: 12,\n          width: 1212,\n          height: 2323,\n          type: ImageSizeType.medium,\n        },\n      ],\n    },\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    id: '1',\n    title: 'The Best Hits of Elton John',\n    images: {\n      main: [\n        {\n          url: 'https://unsplash.it/182/182',\n          fileSize: 12,\n          width: 1212,\n          height: 2323,\n          type: ImageSizeType.medium,\n        },\n      ],\n    },\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistCard/PlaylistCard.tsx",
    "content": "import * as React from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { Link } from 'react-router'\n\nimport type { SchemaPlaylistImagesOutputDto } from '@/shared/api/schema.ts'\nimport { Paths } from '@/shared/config/paths.ts'\nimport {\n  Card,\n  CoverImage,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Typography,\n} from '@/shared/components'\nimport { featuresFlags } from '@/shared/featureFlags.ts'\nimport { useDeletePlaylistAction } from '@/shared/hooks/useDeletePlaylistAction'\nimport { DeleteIcon, EditIcon, MoreIcon } from '@/shared/icons'\nimport { formatCreatedDate, VU } from '@/shared/utils'\n\nimport s from './PlaylistCard.module.scss'\n\ninterface PlaylistCardProps {\n  id: string\n  title?: string\n  images?: SchemaPlaylistImagesOutputDto\n  footer?: React.ReactNode\n  canEdit?: boolean\n  userName?: string\n  userId?: string\n  addedAt?: string\n  tracksCount?: number\n  shouldShowOwnerName?: boolean\n  shouldShowCreatedDate?: boolean\n}\n\nexport const PlaylistCard: React.FC<PlaylistCardProps> = (props) => {\n  const {\n    title,\n    images,\n    id,\n    footer,\n    canEdit = false,\n    userName,\n    userId,\n    addedAt,\n    tracksCount,\n    shouldShowOwnerName = false,\n    shouldShowCreatedDate = false,\n  } = props\n\n  const { t } = useTranslation()\n  const handleDeletePlaylist = useDeletePlaylistAction(id)\n\n  const imageSrc = VU.isNotEmptyArray(images?.main) ? images.main[0].url : undefined\n\n  const handleUserNameClick = (e: React.MouseEvent) => {\n    e.stopPropagation()\n  }\n\n  return (\n    <Card className={s.card}>\n      <Link to={`${Paths.Playlists}/${id}`} className={s.image}>\n        <CoverImage imageSrc={imageSrc} imageDescription={'cover'} aria-hidden />\n      </Link>\n      <div className={s.titleWrapper}>\n        <Typography variant=\"h3\" className={s.title}>\n          {title}\n        </Typography>\n        {canEdit && (\n          <DropdownMenu>\n            <DropdownMenuTrigger>\n              <MoreIcon />\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"start\">\n              <DropdownMenuItem>\n                <EditIcon className={s.menuIcon} />\n                <span>Edit</span>\n              </DropdownMenuItem>\n              {featuresFlags.deletePlaylist && (\n                <DropdownMenuItem onClick={handleDeletePlaylist} className={s.deleteItem}>\n                  <DeleteIcon className={s.menuIcon} />\n                  <span>Delete</span>\n                </DropdownMenuItem>\n              )}\n            </DropdownMenuContent>\n          </DropdownMenu>\n        )}\n      </div>\n      <div className={s.details}>\n        {shouldShowOwnerName && (\n          <div className={s.madeFor}>\n            <Typography variant=\"body2\" as=\"span\" className={s.madeForText}>\n              {t('playlist.made_for')}{' '}\n            </Typography>\n            <Link\n              to={`${Paths.Profile}/${userId}`}\n              className={s.userLink}\n              onClick={handleUserNameClick}>\n              {userName}\n            </Link>\n          </div>\n        )}\n\n        <div className={s.detailsRow}>\n          {tracksCount != null && (\n            <Typography variant=\"body2\" className={s.tracks}>\n              {t('playlist.tracks_count', { count: tracksCount })}\n            </Typography>\n          )}\n          {shouldShowCreatedDate && (\n            <>\n              <span className={s.dot} aria-hidden=\"true\" />\n              <Typography variant=\"body2\" className={s.created}>\n                {formatCreatedDate(addedAt)}\n              </Typography>\n            </>\n          )}\n        </div>\n      </div>\n\n      {footer}\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistCard/index.ts",
    "content": "export * from './PlaylistCard.tsx'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistCardSkeleton/PlaylistCardSkeleton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport { Card, Skeleton } from '@/shared/components'\n\nimport s from '../PlaylistCard/PlaylistCard.module.scss'\n\nexport type PlaylistCardSkeletonProps = {\n  showReactionButtons?: boolean\n  className?: string\n} & ComponentProps<'div'>\n\nexport const PlaylistCardSkeleton = ({\n  showReactionButtons = false,\n  className,\n  ...props\n}: PlaylistCardSkeletonProps) => {\n  return (\n    <Card\n      className={clsx(s.card, showReactionButtons && s.withReactionButtons, className)}\n      {...props}>\n      <Skeleton width=\"100%\" height={153} className={s.image} />\n\n      <Skeleton width=\"80%\" height={16} className={s.title} />\n\n      <Skeleton width=\"60%\" height={14} className={s.description} />\n\n      {showReactionButtons && (\n        <div className={s.reactionButtons}>\n          <Skeleton circle width={28} height={28} />\n          <Skeleton circle width={28} height={28} />\n        </div>\n      )}\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistCardSkeleton/index.ts",
    "content": "export * from './PlaylistCardSkeleton'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistItem/PlaylistItem.tsx",
    "content": "import * as React from 'react'\n\nimport type { PlaylistItemProps } from '@/entities/playlist/ui/PlaylistItem/PlaylistItem.types.ts'\nimport { usePlaylistReactions } from '@/features/playlists/model/usePlaylistReactions'\nimport { ReactionButtons } from '@/shared/components'\n\nimport { PlaylistCard } from '../PlaylistCard'\n\nexport const PlaylistItem: React.FC<PlaylistItemProps> = (props) => {\n  const { playlist } = props\n\n  const { currentUserReaction, title, images, likesCount } = playlist.attributes\n  const { handleLike, handleDislike, handleRemoveReaction } = usePlaylistReactions(playlist.id)\n\n  return (\n    <PlaylistCard\n      id={playlist.id}\n      title={title}\n      images={images}\n      footer={\n        <ReactionButtons\n          currentReaction={currentUserReaction}\n          entityId={playlist.id}\n          likesCount={likesCount}\n          onDislike={handleDislike}\n          onLike={handleLike}\n          onRemoveReaction={handleRemoveReaction}\n        />\n      }\n    />\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistItem/PlaylistItem.types.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\n// duplication of the CurrentUserReaction type to decouple the shared layer from the features layer\nexport type CurrentUserReaction = components['schemas']['ReactionValue']\n\nexport interface PlaylistItemProps {\n  playlist: components['schemas']['PlaylistListItemResource']\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/entities/playlist/ui/PlaylistItem/index.ts",
    "content": "export { PlaylistItem } from './PlaylistItem.tsx'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/artists/api/artists-api.ts",
    "content": "export const MOCK_ARTISTS = [\n  {\n    id: '1',\n    name: 'Kanye West',\n    image: 'https://unsplash.it/148/148',\n  },\n  {\n    id: '2',\n    name: 'Drake & The Weeknd & Kanye West',\n    image: 'https://unsplash.it/149/149',\n  },\n  {\n    id: '3',\n    name: 'Frank Ocean',\n    image: 'https://unsplash.it/150/150',\n  },\n  {\n    id: '4',\n    name: 'Headlund',\n    image: 'https://unsplash.it/151/151',\n  },\n  {\n    id: '5',\n    name: 'Rihanna',\n    image: 'https://unsplash.it/152/152',\n  },\n  {\n    id: '6',\n    name: 'Lamar',\n    image: 'https://unsplash.it/153/153',\n  },\n  {\n    id: '7',\n    name: 'The Weeknd',\n    image: 'https://unsplash.it/154/154',\n  },\n  {\n    id: '8',\n    name: 'Kendrick Lamar',\n    image: 'https://unsplash.it/155/155',\n  },\n  {\n    id: '9',\n    name: 'J. Cole',\n    image: 'https://unsplash.it/156/156',\n  },\n  {\n    id: '10',\n    name: 'Lil Uzi Vert',\n    image: 'https://unsplash.it/157/157',\n  },\n]\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/artists/api/index.ts",
    "content": "export * from './artists-api'\nexport * from './use-artists.query'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/artists/api/use-artists.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\n\nexport type ArtistDto = {\n  id: string\n  name: string\n  image?: string\n}\n\nexport const useArtists = (search?: string) => {\n  return useQuery({\n    queryKey: ['artists', search],\n    queryFn: () => {\n      return getClient().GET('/artists/search', {\n        params: {\n          query: {\n            search: search || '',\n          },\n        },\n      })\n    },\n    select: (response): ArtistDto[] => {\n      return (\n        response.data?.map((artist) => ({\n          id: artist.id,\n          name: artist.name,\n        })) || []\n      )\n    },\n    enabled: true,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/artists/index.ts",
    "content": "export * from './api'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/artists/ui/ArtistCard/ArtistCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n\n  width: 148px;\n  height: 180px;\n}\n\n.image {\n  overflow: hidden;\n\n  width: 148px;\n  height: 148px;\n  border-radius: 50%;\n\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-align: center;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/artists/ui/ArtistCard/ArtistCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ArtistCard } from './ArtistCard'\n\nconst meta: Meta<typeof ArtistCard> = {\n  title: 'entities/ArtistCard',\n  component: ArtistCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof ArtistCard>\n\nexport const Default: Story = {\n  args: {\n    image: 'https://unsplash.it/182/182',\n    name: 'Kanye West',\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    image: 'https://unsplash.it/183/183',\n    name: 'Drake & The Weeknd & Kanye West',\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/artists/ui/ArtistCard/ArtistCard.tsx",
    "content": "import { Typography } from '@/shared/components'\n\nimport s from './ArtistCard.module.css'\n\ntype Props = {\n  image: string\n  name: string\n}\n\nexport const ArtistCard = ({ image, name }: Props) => {\n  return (\n    <div className={s.card}>\n      <div className={s.image}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <Typography variant=\"h3\" className={s.title}>\n        {name}\n      </Typography>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/artists/ui/ArtistCard/index.ts",
    "content": "export * from './ArtistCard'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/api/use-login.mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient, getClientConfig } from '@/shared/api/client.ts'\n\nimport { type LoginRequestPayload } from '../types/auth-api.types'\n\nexport const useLoginMutation = () => {\n  const qc = useQueryClient()\n\n  return useMutation({\n    mutationFn: (payload: LoginRequestPayload) =>\n      getClient().POST('/auth/login', {\n        body: payload,\n      }),\n\n    onSuccess: async (data) => {\n      const cfg = getClientConfig()\n\n      await cfg.saveAccessToken?.(data.data!.accessToken)\n      await cfg.saveRefreshToken?.(data.data!.refreshToken)\n\n      await qc.invalidateQueries()\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/api/use-logout.mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { useProfileStore } from '@/features/profile/model/profile-store'\nimport { getClient } from '@/shared/api/client.ts'\nimport { unwrap } from '@/shared/api/utils/unwrap.ts'\nimport { authStorage } from '@/shared/utils/authStorage.ts'\n\nexport const useLogoutMutation = () => {\n  const qc = useQueryClient()\n  return useMutation({\n    mutationFn: () => {\n      return unwrap(\n        getClient().POST('/auth/logout', {\n          body: {\n            refreshToken: authStorage.getRefreshToken()!,\n          },\n        })\n      )\n    },\n    onSuccess: async () => {\n      authStorage.clearTokens()\n      useProfileStore.getState().resetProfile()\n\n      //qc.clear() // clear удаляет все кэшированные данные\n      await qc.resetQueries({ queryKey: ['auth', 'me'] }) // resetQueries переводит query в изначальное состояние и уведомляет подписчиков — компонент получит data = undefined.\n      //await qc.invalidateQueries({ queryKey: ['auth', 'me'] }) // invalidateQueries заставит его немедленно перефетчиться без токена ⇒ получите 401 ⇒ data станет undefined / error.\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/api/use-me.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\nimport { unwrap } from '@/shared/api/utils/unwrap.ts'\nimport { authStorage } from '@/shared/utils/authStorage.ts'\n\nexport const useMeQuery = () => {\n  // This optimization is nice — it prevents the request when we know for sure\n  // that the user is not authenticated because there are no tokens in storage.\n  // But it breaks the login flow: after login, tokens appear in storage,\n  // yet this hook doesn't know about it, so enabled stays false.\n  // We either drop this optimization or come up with an elegant solution.\n  // const hasAtLeastOneToken = !!authStorage.getRefreshToken() || !!authStorage.getAccessToken()\n\n  return useQuery({\n    queryKey: ['auth', 'me'],\n    queryFn: () => unwrap(getClient().GET('/auth/me')),\n    // enabled: hasAtLeastOneToken,\n    retry: false,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/index.ts",
    "content": "export * from './ui'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/types/auth-api.types.ts",
    "content": "import { getClientConfig } from '@/shared/api/client.ts'\nimport type { components } from '@/shared/api/schema.ts'\n\nexport type RefreshOutput = components['schemas']['RefreshOutput']\n\nexport type RefreshRequestPayload = components['schemas']['RefreshRequestPayload']\n\nexport type LoginRequestPayload = components['schemas']['LoginRequestPayload']\n\nexport const getOauthRedirectUrl = (redirectUrl: string) =>\n  getClientConfig().baseURL + `/auth/oauth-redirect?callbackUrl=${redirectUrl}`\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/LoginButtonAndModal/LoginButtonAndModal.tsx",
    "content": "import { useState } from 'react'\n\nimport { LoginModal } from '@/features/auth/ui/LoginModal'\nimport { Button } from '@/shared/components/Button'\nimport { useTranslation } from 'react-i18next'\n\nexport const LoginButtonAndModal = () => {\n  const [isOpen, setIsOpen] = useState(false)\n  const { t } = useTranslation()\n\n  const handleOpenModal = () => setIsOpen(true)\n  const handleCloseModal = () => setIsOpen(false)\n\n  return (\n    <>\n      <Button variant=\"primary\" onClick={handleOpenModal}>\n        {t('auth.button.sign_in')}\n      </Button>\n      {isOpen && <LoginModal onClose={handleCloseModal} />}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/LoginButtonAndModal/index.ts",
    "content": "export { LoginButtonAndModal } from './LoginButtonAndModal'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/LoginModal/LoginModal.module.css",
    "content": ".dialog {\n  width: 376px;\n  padding-bottom: 22px;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n  align-items: center;\n\n  text-align: center;\n}\n\n.icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 60px;\n  height: 60px;\n  border-radius: 50%;\n\n  font-size: 24px;\n\n  background-color: var(--color-accent);\n}\n\n.button {\n  height: 55px;\n}\n\n.secondary {\n  background-color: #555;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/LoginModal/LoginModal.tsx",
    "content": "import clsx from 'clsx'\n\nimport { useLoginMutation } from '@/features/auth/api/use-login.mutation.ts'\nimport { getOauthRedirectUrl } from '@/features/auth/types/auth-api.types.ts'\nimport s from '@/features/auth/ui/LoginModal/LoginModal.module.css'\nimport { Button, Dialog, DialogContent, DialogHeader, Typography } from '@/shared/components'\nimport { CURRENT_APP_DOMAIN } from '@/shared/config/config.ts'\nimport { joinUrl } from '@/shared/utils/join-url.ts'\nimport { useTranslation } from 'react-i18next'\n\ntype Props = {\n  onClose: () => void\n}\n\nexport const LoginModal = ({ onClose }: Props) => {\n  const { mutate } = useLoginMutation()\n  const { t } = useTranslation()\n\n  const loginHandler = () => {\n    const segments = [window.location.origin]\n    if (CURRENT_APP_DOMAIN) {\n      segments.push(CURRENT_APP_DOMAIN)\n    }\n    segments.push('oauth/callback')\n\n    const redirectUri = joinUrl(...segments)\n    const url = getOauthRedirectUrl(redirectUri)\n    window.open(url, 'oauthPopup', 'width=500,height=600')\n\n    const receiveMessage = async (event: MessageEvent) => {\n      if (event.origin !== window.location.origin) {\n        return\n      }\n\n      const { code } = event.data\n      if (code) {\n        console.log('✅ code received:', code)\n        // тут можно вызвать setToken(accessToken) или dispatch(login)\n        //popup?.close()\n        window.removeEventListener('message', receiveMessage)\n        mutate({\n          code,\n          accessTokenTTL: '10s',\n          redirectUri,\n          rememberMe: true,\n        })\n        onClose()\n      }\n    }\n\n    window.addEventListener('message', receiveMessage)\n  }\n\n  return (\n    <Dialog open onClose={onClose} className={s.dialog}>\n      <DialogHeader />\n\n      <DialogContent className={s.content}>\n        <Typography variant=\"h2\">{t('auth.modal.title')}</Typography>\n\n        <div className={s.icon}>😊</div>\n\n        <Button className={clsx(s.button, s.secondary)} fullWidth onClick={onClose}>\n          {t('auth.button.continue_without_sign_in')}\n        </Button>\n        <Button\n          as=\"button\"\n          target=\"_blank\"\n          className={s.button}\n          variant=\"primary\"\n          fullWidth\n          onClick={loginHandler}>\n          {t('auth.button.sign_in_with_apihub')}\n        </Button>\n      </DialogContent>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/LoginModal/index.ts",
    "content": "export { LoginModal } from './LoginModal.tsx'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.module.css",
    "content": ".trigger {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  gap: 11px;\n  align-items: center;\n\n  padding: 3px 39px 3px 3px;\n  border-radius: 40px;\n}\n\n.trigger::after {\n  content: '';\n\n  position: absolute;\n  top: 50%;\n  right: 12px;\n  transform: translateY(-50%);\n\n  display: block;\n\n  width: 14px;\n  height: 7px;\n\n  background-color: var(--color-text-primary);\n  clip-path: polygon(50% 100%, 0 0, 100% 0);\n\n  transition: clip-path 0.3s ease;\n}\n\n.trigger[data-open]::after {\n  clip-path: polygon(50% 0, 0 100%, 100% 100%);\n}\n\n.avatar {\n  overflow: hidden;\n\n  width: 34px;\n  height: 34px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-xxxs);\n}\n\n.name {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ProfileDropdownMenu } from './ProfileDropdownMenu'\n\nconst meta: Meta<typeof ProfileDropdownMenu> = {\n  title: 'entities/ProfileDropdownMenu',\n  component: ProfileDropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof ProfileDropdownMenu>\n\nexport const Default: Story = {\n  args: {\n    avatar: null,\n    fullName: { name: '', surname: '' },\n    userLogin: 'demo-user',\n    id: '1',\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { Link } from 'react-router'\n\nimport { useLogoutMutation } from '@/features/auth/api/use-logout.mutation.ts'\nimport type { FullName } from '@/features/profile'\nimport {\n  Avatar,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Typography,\n} from '@/shared/components'\nimport { LogoutIcon, ProfileIcon } from '@/shared/icons'\n\nimport s from './ProfileDropdownMenu.module.css'\n\ntype ProfileDropdownMenuProps = {\n  avatar: string | null\n  fullName: FullName\n  userLogin: string\n  id: string\n}\n\nexport const ProfileDropdownMenu = ({\n  avatar,\n  fullName,\n  userLogin,\n  id,\n}: ProfileDropdownMenuProps) => {\n  const logoutMutation = useLogoutMutation()\n  const { t } = useTranslation()\n  const profileName = fullName?.name ? `${fullName.name} ${fullName.surname}` : userLogin\n\n  const handleLogout = () => {\n    logoutMutation.mutate()\n  }\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild className={s.trigger}>\n        <Avatar className={s.avatar} src={avatar} fullName={fullName} userLogin={userLogin} />\n        <Typography className={s.name} variant=\"body2\">\n          {profileName}\n        </Typography>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem as={Link} to={`/user/${id}`}>\n          <ProfileIcon />\n          <span>{t('auth.title.my_profile')}</span>\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={handleLogout} disabled={logoutMutation.isPending}>\n          <LogoutIcon />\n          <span>\n            {logoutMutation.isPending ? t('auth.button.logging_out') : t('auth.title.logout')}\n          </span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/ProfileDropdownMenu/index.ts",
    "content": "export * from './ProfileDropdownMenu'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/auth/ui/index.ts",
    "content": "export * from './LoginButtonAndModal'\nexport * from './ProfileDropdownMenu'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/api/index.ts",
    "content": "export * from './playlistsApi'\nexport * from './use-playlists.query'\nexport * from './use-playlist.query'\nexport * from './use-playlist-mutations'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/api/playlistsApi.ts",
    "content": "import { getClient } from '@/shared/api/client.ts'\nimport type { SchemaReactionOutput } from '@/shared/api/schema.ts'\nimport { ImageSizeType, ReactionValue, type SchemaGetPlaylistOutput } from '@/shared/api/schema.ts'\n\nimport { CurrentUserReaction } from './types'\n\nexport const playlistsApi = {\n  likePlaylist: (playlistId: SchemaReactionOutput['objectId']) => {\n    return getClient().POST('/playlists/{playlistId}/likes', {\n      params: {\n        path: {\n          playlistId,\n        },\n      },\n    })\n  },\n\n  dislikePlaylist: (playlistId: SchemaReactionOutput['objectId']) => {\n    return getClient().POST('/playlists/{playlistId}/dislikes', {\n      params: {\n        path: {\n          playlistId,\n        },\n      },\n    })\n  },\n\n  removePlaylistReaction: (playlistId: SchemaReactionOutput['objectId']) => {\n    return getClient().DELETE('/playlists/{playlistId}/reactions', {\n      params: {\n        path: {\n          playlistId,\n        },\n      },\n    })\n  },\n}\n\nexport const MOCK_PLAYLISTS: SchemaGetPlaylistOutput[] = [\n  {\n    data: {\n      id: '1',\n      type: 'playlists',\n      attributes: {\n        title: 'Chill Vibes',\n        description: 'Relax and unwind with these chill tracks 🌊',\n        addedAt: '2025-06-01T12:00:00Z',\n        updatedAt: '2025-06-10T15:30:00Z',\n        order: 1,\n        user: { id: 'user-101', name: 'Alice' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 640,\n              height: 640,\n              fileSize: 204800,\n              url: 'https://unsplash.it/183/183',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'chill' },\n          { id: '2', name: 'lofi' },\n          { id: '3', name: 'relax' },\n        ],\n        currentUserReaction: ReactionValue.Value1,\n        likesCount: 542,\n        dislikesCount: 2,\n        tracksCount: 12,\n      },\n    },\n  },\n  {\n    data: {\n      id: '2',\n      type: 'playlists',\n      attributes: {\n        title: 'Workout Pump',\n        description: 'High energy tracks to keep you moving 💪',\n        addedAt: '2025-05-20T08:00:00Z',\n        updatedAt: '2025-06-05T18:00:00Z',\n        order: 2,\n        user: { id: 'user-202', name: 'Bob' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 800,\n              height: 800,\n              fileSize: 307200,\n              url: 'https://unsplash.it/184/184',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'fitness' },\n          { id: '2', name: 'pump' },\n          { id: '3', name: 'motivation' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 123,\n        dislikesCount: 9,\n        tracksCount: 8,\n      },\n    },\n  },\n  {\n    data: {\n      id: '3',\n      type: 'playlists',\n      attributes: {\n        title: 'Fantasy Soundtrack',\n        description: 'Epic and magical music for your quests 🏹',\n        addedAt: '2025-04-15T14:30:00Z',\n        updatedAt: '2025-05-01T10:10:00Z',\n        order: 3,\n        user: { id: 'user-303', name: 'Elrond' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 1024,\n              height: 768,\n              fileSize: 512000,\n              url: 'https://unsplash.it/185/185',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'fantasy' },\n          { id: '2', name: 'soundtrack' },\n          { id: '3', name: 'epic' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 54,\n        dislikesCount: 7,\n        tracksCount: 15,\n      },\n    },\n  },\n  {\n    data: {\n      id: '4',\n      type: 'playlists',\n      attributes: {\n        title: 'Suffer possible assume',\n        description: 'Recently religious responsibility whether only.',\n        addedAt: '2025-04-29T10:39:13',\n        updatedAt: '2025-06-14T21:01:35',\n        order: 4,\n        user: { id: 'user-4', name: 'Katie' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 936,\n              height: 306,\n              fileSize: 243840,\n              url: 'https://unsplash.it/192/192',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'any' },\n          { id: '2', name: 'shake' },\n          { id: '3', name: 'white' },\n        ],\n        currentUserReaction: ReactionValue.Value1,\n        likesCount: 3,\n        dislikesCount: 4,\n        tracksCount: 5,\n      },\n    },\n  },\n  {\n    data: {\n      id: '5',\n      type: 'playlists',\n      attributes: {\n        title: 'Risk still',\n        description: 'Skin pay sure yeah couple live heart.',\n        addedAt: '2025-01-26T00:52:16',\n        updatedAt: '2025-06-14T21:00:56',\n        order: 5,\n        user: { id: 'user-5', name: 'Robert' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 525,\n              height: 500,\n              fileSize: 185000,\n              url: 'https://unsplash.it/191/191',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'term' },\n          { id: '2', name: 'item' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 14,\n        dislikesCount: 12,\n        tracksCount: 6,\n      },\n    },\n  },\n  {\n    data: {\n      id: '6',\n      type: 'playlists',\n      attributes: {\n        title: 'Attack through go',\n        description: 'Plan deep sport growth tonight.',\n        addedAt: '2025-04-07T10:16:19',\n        updatedAt: '2025-06-14T21:02:28',\n        order: 6,\n        user: { id: 'user-6', name: 'Shelly' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 985,\n              height: 44,\n              fileSize: 105000,\n              url: 'https://unsplash.it/190/190',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'feeling' },\n          { id: '2', name: 'size' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 0,\n        dislikesCount: 2,\n        tracksCount: 0,\n      },\n    },\n  },\n  {\n    data: {\n      id: '7',\n      type: 'playlists',\n      attributes: {\n        title: 'Yet woman outside',\n        description: 'Attorney especially child music capital well.',\n        addedAt: '2025-01-02T16:37:47',\n        updatedAt: '2025-06-14T21:03:26',\n        order: 7,\n        user: { id: 'user-7', name: 'Kristopher' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 541,\n              height: 589,\n              fileSize: 312000,\n              url: 'https://unsplash.it/189/189',\n            },\n          ],\n        },\n        tags: [{ id: '1', name: 'week' }],\n        currentUserReaction: ReactionValue.Value1,\n        likesCount: 12,\n        dislikesCount: 1,\n        tracksCount: 3,\n      },\n    },\n  },\n  {\n    data: {\n      id: '8',\n      type: 'playlists',\n      attributes: {\n        title: 'Community',\n        description: 'Visit about occur it fast industry process.',\n        addedAt: '2025-06-03T22:12:23',\n        updatedAt: '2025-06-14T21:00:31',\n        order: 8,\n        user: { id: 'user-8', name: 'Kimberly' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 376,\n              height: 803,\n              fileSize: 460000,\n              url: 'https://unsplash.it/188/188',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'serve' },\n          { id: '2', name: 'although' },\n          { id: '3', name: 'item' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 12,\n        dislikesCount: 14,\n        tracksCount: 14,\n      },\n    },\n  },\n  {\n    data: {\n      id: '9',\n      type: 'playlists',\n      attributes: {\n        title: 'Dance Lights Forever',\n        description: 'Feel the beat drop and the lights flash 🎉',\n        addedAt: '2024-12-14T15:20:12',\n        updatedAt: '2025-06-13T17:15:00',\n        order: 9,\n        user: { id: 'user-9', name: 'Jasmine' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 800,\n              height: 800,\n              fileSize: 310000,\n              url: 'https://unsplash.it/187/187',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'dance' },\n          { id: '2', name: 'party' },\n          { id: '3', name: 'electro' },\n        ],\n        currentUserReaction: ReactionValue.Value0,\n        likesCount: 2,\n        dislikesCount: 14,\n        tracksCount: 20,\n      },\n    },\n  },\n  {\n    data: {\n      id: '10',\n      type: 'playlists',\n      attributes: {\n        title: 'Calm Forest Ambience',\n        description: 'Let nature help you concentrate 🌲',\n        addedAt: '2025-03-01T09:45:00',\n        updatedAt: '2025-06-10T13:20:00',\n        order: 10,\n        user: { id: 'user-10', name: 'Leo' },\n        images: {\n          main: [\n            {\n              type: ImageSizeType.original,\n              width: 1024,\n              height: 576,\n              fileSize: 280000,\n              url: 'https://unsplash.it/186/186',\n            },\n          ],\n        },\n        tags: [\n          { id: '1', name: 'nature' },\n          { id: '2', name: 'focus' },\n          { id: '3', name: 'relax' },\n        ],\n        currentUserReaction: ReactionValue.ValueMinus1,\n        likesCount: 84,\n        dislikesCount: 14,\n        tracksCount: 30,\n      },\n    },\n  },\n]\n\nexport const MOCK_PLAYLIST: SchemaGetPlaylistOutput = {\n  data: {\n    id: '10',\n    type: 'playlists',\n    attributes: {\n      title: 'Calm Forest Ambience',\n      description: 'Let nature help you concentrate 🌲',\n      addedAt: '2025-03-01T09:45:00',\n      updatedAt: '2025-06-10T13:20:00',\n      order: 10,\n      user: {\n        id: 'user-10',\n        name: 'Leo',\n      },\n      images: {\n        main: [\n          {\n            type: ImageSizeType.original,\n            width: 1024,\n            height: 576,\n            fileSize: 280000,\n            url: 'https://unsplash.it/300/300',\n          },\n        ],\n      },\n      tags: [\n        { id: '1', name: 'nature' },\n        { id: '2', name: 'focus' },\n        { id: '3', name: 'relax' },\n      ],\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 12,\n      dislikesCount: 0,\n      tracksCount: 30,\n    },\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/api/query-key-factory.ts",
    "content": "import type { SchemaGetPlaylistsRequestPayload } from '@/shared/api/schema.ts'\n\nexport const playlistsKeys = {\n  all: ['playlists'] as const, // playlists\n  lists: () => [...playlistsKeys.all, 'list'] as const, //  playlists, list\n  list: (filters: Partial<SchemaGetPlaylistsRequestPayload>) =>\n    [...playlistsKeys.lists(), filters] as const, //  playlists, list, {:filter}\n  details: () => [...playlistsKeys.all, 'detail'] as const, // playlists, detail\n  detail: (id: string) => [...playlistsKeys.details(), id] as const, // playlists, details, :id\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/api/types.ts",
    "content": "export enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = -1,\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/api/use-playlist-mutations.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { getClient } from '@/shared/api/client'\nimport { playlistsKeys } from './query-key-factory'\nimport type {\n  SchemaCreatePlaylistRequestPayload,\n  SchemaUpdatePlaylistRequestPayload,\n} from '@/shared/api/schema'\n\nexport const useCreatePlaylistMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: (payload: SchemaCreatePlaylistRequestPayload) =>\n      getClient().POST('/playlists', { body: payload }),\n    onSuccess: () => {\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n    },\n  })\n}\n\nexport const useUpdatePlaylistMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: ({\n      playlistId,\n      payload,\n    }: {\n      playlistId: string\n      payload: SchemaUpdatePlaylistRequestPayload\n    }) =>\n      getClient().PUT('/playlists/{playlistId}', {\n        params: { path: { playlistId } },\n        body: payload,\n      }),\n    onSuccess: (_, { playlistId }) => {\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.detail(playlistId) })\n    },\n  })\n}\n\nexport const useUploadPlaylistCoverMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: ({ playlistId, file }: { playlistId: string; file: File }) => {\n      const formData = new FormData()\n      formData.append('file', file)\n      return getClient().POST('/playlists/{playlistId}/images/main', {\n        params: { path: { playlistId } },\n        body: formData as any,\n        bodySerializer: (body) => body, // Don't serialize FormData as JSON\n      })\n    },\n    onSuccess: (_, { playlistId }) => {\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.detail(playlistId) })\n    },\n  })\n}\n\nexport const useDeletePlaylistMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: (playlistId: string) =>\n      getClient().DELETE('/playlists/{playlistId}', { params: { path: { playlistId } } }),\n    onSuccess: () => {\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/api/use-playlist.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { playlistsKeys } from './query-key-factory'\nimport { getClient } from '@/shared/api/client'\n\nexport const usePlaylist = (playlistId: string) => {\n  return useQuery({\n    queryKey: playlistsKeys.detail(playlistId),\n    queryFn: async () => {\n      const response = await getClient().GET('/playlists/{playlistId}', {\n        params: { path: { playlistId } },\n      })\n      return response.data\n    },\n    enabled: !!playlistId,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/api/use-playlists.query.ts",
    "content": "import { keepPreviousData, useQuery } from '@tanstack/react-query'\n\nimport { playlistsKeys } from '@/features/playlists/api/query-key-factory.ts'\nimport { getClient } from '@/shared/api/client.ts'\nimport type { SchemaGetPlaylistsRequestPayload } from '@/shared/api/schema.ts'\nimport { VU } from '@/shared/utils'\n\nexport const usePlaylists = (\n  params: Partial<SchemaGetPlaylistsRequestPayload>,\n  opts?: { enabled?: boolean }\n) => {\n  const query = useQuery({\n    queryKey: playlistsKeys.list(params),\n    queryFn: () => {\n      return getClient().GET('/playlists', {\n        params: {\n          query: {\n            ...params,\n            search: params.search || undefined,\n            tagsIds: VU.isValid(params.tagsIds) ? params.tagsIds : undefined,\n          },\n        },\n      })\n    },\n    enabled: opts?.enabled ?? true,\n    placeholderData: keepPreviousData,\n  })\n\n  return query\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/model/usePlaylistReactions.ts",
    "content": "import { useEntityReactions } from '@/shared/hooks/useEntityReactions'\n\nimport { playlistsApi } from '../api/playlistsApi'\nimport { playlistsKeys } from '../api/query-key-factory'\n\nexport const usePlaylistReactions = (playlistId: string) =>\n  useEntityReactions({\n    entityId: playlistId,\n    api: {\n      like: playlistsApi.likePlaylist,\n      dislike: playlistsApi.dislikePlaylist,\n      remove: playlistsApi.removePlaylistReaction,\n    },\n    keys: playlistsKeys,\n  })\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/ChoosePlaylistModal/ChoosePlaylistModal.module.css",
    "content": ".playlistList {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 24px;\n\n  margin: 0;\n  padding: 0;\n\n  list-style: none;\n}\n\n.playlistItem {\n  cursor: pointer;\n\n  display: flex;\n\n  padding: 12px;\n  border: 2px solid transparent;\n\n  background: #18181b;\n\n  transition:\n    border 0.2s,\n    opacity 0.2s,\n    background-color 0.2s;\n}\n\n.playlistItem:not(.selected):focus-within,\n.playlistItem:not(.selected):hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.selected {\n  opacity: 1;\n  background-color: rgb(255 56 182 / 30%);\n  outline: 1px solid #e052c6;\n  box-shadow: 0 0 0 2px #e052c6;\n}\n\n.playlistLabel {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n\n  width: 100%;\n\n  outline: none;\n}\n\n.imageWrapper {\n  overflow: hidden;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 120px;\n  height: 120px;\n  margin-bottom: 12px;\n  border-radius: 8px;\n\n  background: #222;\n}\n\n.imageWrapper img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.playlistTitle {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.playlistTracks {\n  font-size: 0.9rem;\n  color: #b3b3b3;\n}\n\n/* hidden checkbox for accessibility */\n.playlistLabel input[type='checkbox'] {\n  pointer-events: none;\n\n  position: absolute;\n\n  width: 1px;\n  height: 1px;\n\n  opacity: 0;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/ChoosePlaylistModal/ChoosePlaylistModal.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { usePlaylists } from '@/features/playlists/api/use-playlists.query'\nimport noCoverPlaceholder from '@/assets/img/no-cover-placeholder.avif'\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  Typography,\n} from '@/shared/components'\n\nimport s from './ChoosePlaylistModal.module.css'\n\nexport const ChoosePlaylistModal = ({\n  playlistIds,\n  isOpen,\n  setIsOpen,\n  setPlaylistIds,\n  onChoose,\n}: {\n  isOpen: boolean\n  setIsOpen: (isOpen: boolean) => void\n  playlistIds: string[]\n  setPlaylistIds: (playlistIds: string[]) => void\n  onChoose?: () => void\n}) => {\n  const { t } = useTranslation()\n\n  const { data: user } = useMeQuery()\n\n  const { data: playlistsResponse } = usePlaylists(\n    {\n      userId: user?.userId,\n    },\n    { enabled: isOpen }\n  )\n\n  const playlists = playlistsResponse?.data?.data || []\n\n  function handleToggle(id: string) {\n    if (playlistIds.includes(id)) {\n      setPlaylistIds(playlistIds.filter((pid) => pid !== id))\n    } else {\n      setPlaylistIds([...playlistIds, id])\n    }\n  }\n\n  return (\n    <Dialog\n      open={isOpen}\n      onClose={() => setIsOpen(false)}\n      aria-modal=\"true\"\n      aria-labelledby=\"choose-playlist-title\">\n      <DialogHeader>\n        <Typography variant=\"h2\">Choose playlist</Typography>\n      </DialogHeader>\n      <DialogContent>\n        <ul className={s.playlistList}>\n          {playlists.map((playlist) => {\n            const image = playlist.attributes.images.main?.[0]?.url\n            const checked = playlistIds.includes(playlist.id)\n            return (\n              <li key={playlist.id} className={s.playlistItem + (checked ? ' ' + s.selected : '')}>\n                <label\n                  className={s.playlistLabel}\n                  tabIndex={0}\n                  onKeyDown={(e) => {\n                    if (e.key === ' ' || e.key === 'Enter') {\n                      e.preventDefault()\n                      handleToggle(playlist.id)\n                    }\n                  }}>\n                  <input\n                    type=\"checkbox\"\n                    checked={checked}\n                    onChange={() => handleToggle(playlist.id)}\n                    tabIndex={-1}\n                    aria-checked={checked}\n                    aria-label={`Select playlist ${playlist.attributes.title}`}\n                  />\n                  <div className={s.imageWrapper}>\n                    <img src={image || noCoverPlaceholder} alt={playlist.attributes.title} />\n                  </div>\n                  <Typography variant=\"h3\" className={s.playlistTitle}>\n                    {playlist.attributes.title}\n                  </Typography>\n                </label>\n              </li>\n            )\n          })}\n        </ul>\n      </DialogContent>\n      <DialogFooter>\n        <Button variant=\"secondary\" onClick={() => setIsOpen(false)}>\n          Cancel\n        </Button>\n        <Button\n          variant=\"primary\"\n          onClick={() => {\n            setIsOpen(false)\n            onChoose?.()\n          }}\n          disabled={playlistIds.length === 0}>\n          {t('button.choose')}\n        </Button>\n      </DialogFooter>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/CreatePlaylistModal/CreatePlaylistModal.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 745px;\n}\n\n.form {\n  overflow-y: auto;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n  margin-bottom: 16px;\n}\n\n.imageUploader {\n  width: 280px;\n  margin: 0 auto;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/CreatePlaylistModal/CreatePlaylistModal.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { type SubmitHandler, useForm } from 'react-hook-form'\nimport { useTranslation } from 'react-i18next'\nimport { toast } from 'react-toastify'\n\nimport { useUpdatePlaylistMutation } from '@/features/playlists/api/use-playlist-mutations'\nimport { usePlaylist } from '@/features/playlists/api/use-playlist.query'\nimport { useCreatePlaylist } from '@/pages/PlaylistsPage/model/useCreatePlaylist'\nimport { useUploadPlaylistCover } from '@/pages/PlaylistsPage/model/useUploadPlaylistCover'\nimport type { SchemaCreatePlaylistAttributes } from '@/shared/api/schema'\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  ImageUploader,\n  TagEditor,\n  Textarea,\n  TextField,\n  Typography,\n} from '@/shared/components'\nimport { useUIStore } from '@/shared/model/ui-store'\n\nimport s from './CreatePlaylistModal.module.css'\n\nexport const CreatePlaylistModal = ({ onClose }: { onClose: () => void }) => {\n  const { t } = useTranslation()\n  const { editingPlaylistId } = useUIStore()\n  const isEditMode = !!editingPlaylistId\n\n  const { mutate } = useCreatePlaylist()\n  const { mutate: updatePlaylist } = useUpdatePlaylistMutation()\n  const { data: playlistResponse } = usePlaylist(editingPlaylistId || '')\n  const playlist = playlistResponse?.data\n  const playlistCoverUrl = playlist?.attributes.images.main?.[0]?.url\n  const { mutate: uploadPlaylistCover } = useUploadPlaylistCover()\n  const [selectedFile, setSelectedFile] = useState<File | null>(null)\n  const [tags, setTags] = useState<string[]>([])\n\n  const handleTagsChange = (tags: string[]) => {\n    setTags(tags)\n  }\n  const { register, handleSubmit, reset } = useForm<SchemaCreatePlaylistAttributes>()\n\n  useEffect(() => {\n    if (!isEditMode || !playlist) {\n      return\n    }\n\n    reset({\n      title: playlist.attributes.title,\n      description: playlist.attributes.description || '',\n    })\n    setTags(playlist.attributes.tags.map((tag) => tag.name))\n  }, [isEditMode, playlist, reset])\n\n  const onSubmit: SubmitHandler<SchemaCreatePlaylistAttributes> = (data) => {\n    if (isEditMode) {\n      if (!editingPlaylistId || !playlist) {\n        return\n      }\n\n      const payload = {\n        data: {\n          type: 'playlists',\n          attributes: {\n            title: data.title,\n            description: data.description || null,\n            tagIds: playlist.attributes.tags.map((tag) => tag.id),\n          },\n        },\n      } as const\n\n      updatePlaylist(\n        { playlistId: editingPlaylistId, payload },\n        {\n          onSuccess: () => {\n            if (selectedFile) {\n              uploadPlaylistCover(\n                { playlistId: editingPlaylistId, file: selectedFile },\n                {\n                  onSettled: () => {\n                    onClose()\n                    reset()\n                    setSelectedFile(null)\n                  },\n                }\n              )\n              return\n            }\n            onClose()\n            reset()\n          },\n        }\n      )\n      return\n    }\n\n    const formData = {\n      ...data,\n      tags,\n    }\n\n    mutate(formData, {\n      onSuccess: (response) => {\n        const playlistId = response?.id\n\n        if (selectedFile && playlistId) {\n          uploadPlaylistCover(\n            {\n              playlistId,\n              file: selectedFile,\n            },\n            {\n              onSuccess: () => {\n                onClose()\n                toast('Success Upload', {\n                  type: 'success',\n                  theme: 'colored',\n                })\n                setSelectedFile(null)\n                reset()\n              },\n              onError: () => {\n                onClose()\n                toast('Upload without image. Not correct size', {\n                  type: 'warning',\n                  theme: 'colored',\n                })\n                setSelectedFile(null)\n                reset()\n              },\n            }\n          )\n        } else {\n          onClose()\n          reset()\n          toast('Success Upload w/o image', {\n            type: 'success',\n            theme: 'colored',\n          })\n        }\n      },\n    })\n  }\n\n  const handleImageSelect = (file: File) => {\n    setSelectedFile(file)\n  }\n\n  return (\n    <Dialog open onClose={onClose} className={s.dialog}>\n      <DialogHeader>\n        <Typography variant=\"h2\">\n          {isEditMode ? t('button.edit') : t('playlists.title.create_playlist')}\n        </Typography>\n      </DialogHeader>\n\n      <form className={s.form} onSubmit={handleSubmit(onSubmit)}>\n        <DialogContent className={s.content}>\n          <ImageUploader\n            className={s.imageUploader}\n            onImageSelect={handleImageSelect}\n            enableCrop\n            cropShape=\"rect\"\n            initialImageUrl={isEditMode ? playlistCoverUrl : undefined}\n          />\n          <TextField\n            label={t('title.title')}\n            placeholder={t('playlists.placeholder.enter_playlist_title')}\n            {...register('title', { required: true })}\n          />\n          <Textarea\n            rows={3}\n            label={t('description.label.description')}\n            placeholder={t('playlists.placeholder.enter_playlist_description')}\n            {...register('description', { required: true })}\n          />\n          <TagEditor\n            label={t('tags.label')}\n            value={tags}\n            onTagsChange={handleTagsChange}\n            maxTags={5}\n          />\n        </DialogContent>\n\n        <DialogFooter>\n          <Button variant=\"secondary\" onClick={onClose} type=\"button\">\n            {t('button.cancel')}\n          </Button>\n          <Button variant=\"primary\" type=\"submit\">\n            {t('button.create')}\n          </Button>\n        </DialogFooter>\n      </form>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/CreatePlaylistModal/index.ts",
    "content": "export * from './CreatePlaylistModal'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.module.css",
    "content": ".container {\n  display: flex;\n  gap: 24px;\n  background: transparent;\n}\n\n.imageContainer {\n  flex-shrink: 0;\n  width: 297px;\n  height: 297px;\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  box-shadow: 0 4px 60px rgba(0, 0, 0, 0.5);\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  min-width: 0;\n  justify-content: flex-end;\n}\n\n.title {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  margin-bottom: 8px;\n\n  font-size: clamp(var(--font-size-xxl), 8vw, var(--font-size-xxxl));\n  font-weight: 900;\n  white-space: pre-wrap;\n  line-height: 1;\n}\n\n.description {\n  opacity: 0.7;\n  margin-bottom: 8px;\n}\n\n.info {\n  margin-top: 8px;\n  overflow-wrap: break-word;\n}\n\n.meta {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n}\n\n.dot {\n  opacity: 0.7;\n}\n\n.userName strong {\n  font-weight: 600;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_5_HASHTAGS } from '@/features/tags/api/tags-api'\n\nimport { PlaylistOverview } from '../PlaylistOverview'\n\nconst MOCK_TAGS = MOCK_5_HASHTAGS.map((name, index) => ({ id: String(index), name }))\n\nconst meta: Meta<typeof PlaylistOverview> = {\n  title: 'entities/PlaylistOverview',\n  component: PlaylistOverview,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistOverview>\n\nexport const Default: Story = {\n  args: {\n    title: 'Chill Mix',\n    image: 'https://unsplash.it/297/297',\n    description: 'Julia Wolf, ayokay, Khalid and more',\n    tags: MOCK_TAGS,\n  },\n}\n\nexport const LongTitle: Story = {\n  args: {\n    title: 'This is a Very Long Playlist Title That Should Scale Responsively',\n    image: 'https://unsplash.it/299/299',\n    description: 'A collection of amazing tracks from various artists around the world',\n    tags: MOCK_TAGS,\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.tsx",
    "content": "import clsx from 'clsx'\nimport { type ComponentProps } from 'react'\n\nimport { type TagDto, TagsList } from '@/features/tags'\nimport { CoverImage, Typography } from '@/shared/components'\n\nimport { useTranslation } from 'react-i18next'\n\nimport s from './PlaylistOverview.module.css'\n\ntype PlaylistOverviewProps = {\n  title: string\n  image: string\n  description: string\n  tags: TagDto[]\n  userName?: string\n  tracksCount?: number\n} & ComponentProps<'div'>\n\nexport const PlaylistOverview = ({\n  title,\n  image,\n  description,\n  tags,\n  className,\n  userName,\n  tracksCount,\n  ...props\n}: PlaylistOverviewProps) => {\n  const { t } = useTranslation()\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <div className={s.imageContainer}>\n        <CoverImage imageSrc={image} imageDescription={'cover'} aria-hidden />\n      </div>\n\n      <div className={s.content}>\n        <TagsList tags={tags} entity=\"playlists\" />\n\n        <Typography variant=\"h1\" as=\"h1\" className={s.title}>\n          {title}\n        </Typography>\n\n        <div className={s.info}>\n          <Typography variant=\"body1\" className={s.description}>\n            {description}\n          </Typography>\n          <div className={s.meta}>\n            {userName && (\n              <Typography variant=\"body2\" as=\"span\" className={s.userName}>\n                {t('playlist.made_for')} <strong>{userName}</strong>\n              </Typography>\n            )}\n            {tracksCount !== undefined && (\n              <>\n                <span className={s.dot}>•</span>\n                <Typography variant=\"body2\" as=\"span\" className={s.tracksCount}>\n                  {t('playlist.tracks_count', { count: tracksCount })}\n                </Typography>\n              </>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/PlaylistOverview/index.ts",
    "content": "export * from './PlaylistOverview'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/PlaylistRow/PlaylistRow.module.css",
    "content": ".playlistRow {\n  display: flex;\n  gap: 20px;\n  align-items: center;\n\n  width: 100%;\n  padding: 8px 16px;\n}\n\n.playlistLink {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  min-width: 400px;\n}\n\n.playlistRow:hover {\n  border-radius: 4px;\n  background-color: var(--color-accent);\n}\n\n.image {\n  width: 52px;\n  height: 52px;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  flex-grow: 1;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.trackCounts {\n  cursor: default;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/PlaylistRow/PlaylistRow.tsx",
    "content": "import clsx from 'clsx'\nimport { Link } from 'react-router'\nimport { useTranslation } from 'react-i18next'\n\nimport noCoverPlaceholder from '@/assets/img/no-cover-placeholder.avif'\nimport { Typography } from '@/shared/components'\n\nimport s from './PlaylistRow.module.css'\n\ntype PlaylistRowProps = {\n  id: string\n  title: string\n  imageSrc?: string\n  className?: string\n}\n\nexport const PlaylistRow = ({\n  title,\n  imageSrc = noCoverPlaceholder,\n  id,\n  className,\n}: PlaylistRowProps) => {\n  const { t } = useTranslation()\n\n  return (\n    <div className={clsx(s.playlistRow, className)}>\n      <Link to={`/playlists/${id}`} className={s.playlistLink}>\n        <div className={s.image}>\n          <img src={imageSrc} alt={title} />\n        </div>\n\n        <div>\n          <Typography variant=\"body1\" as=\"h2\" className={s.title}>\n            {title}\n          </Typography>\n        </div>\n      </Link>\n\n      <div className={s.trackCounts}>\n        <Typography variant=\"body2\" as=\"span\">\n          {t('playlist.tracks_count', { count: 0 })}\n        </Typography>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/PlaylistRow/index.ts",
    "content": "export * from './PlaylistRow'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/playlists/ui/index.ts",
    "content": "export * from './CreatePlaylistModal'\nexport * from './PlaylistOverview'\nexport * from './ChoosePlaylistModal/ChoosePlaylistModal'\nexport * from './PlaylistRow'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/config/empty-profile.ts",
    "content": "import type { Profile } from '../types/profile.types'\n\nexport const emptyProfile: Profile = {\n  avatar: null,\n  fullName: { name: '', surname: '' },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/config/index.ts",
    "content": "export * from './empty-profile'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/index.ts",
    "content": "export * from './config'\nexport * from './model'\nexport * from './types'\nexport * from './ui'\nexport * from './utils'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/model/hooks/index.ts",
    "content": "export * from './use-edit-profile-modal'\nexport * from './use-edit-profile-schema'\nexport * from './use-hydrate-profile'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/model/hooks/use-edit-profile-modal.ts",
    "content": "import { useCallback } from 'react'\n\nimport { useProfileStore } from '../profile-store'\n\nexport const useEditProfileModal = () => {\n  const isEditProfileOpen = useProfileStore((state) => state.isEditProfileModalOpen)\n  const setEditProfileModalOpen = useProfileStore((state) => state.setEditProfileModalOpen)\n\n  const handleOpenEditProfileModal = useCallback(() => {\n    setEditProfileModalOpen(true)\n  }, [setEditProfileModalOpen])\n\n  return {\n    isEditProfileOpen,\n    handleOpenEditProfileModal,\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/model/hooks/use-edit-profile-schema.ts",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { editProfileSchemaBase } from '../profile-schemas'\n\nexport const useEditProfileSchema = () => {\n  const { t } = useTranslation()\n\n  const editProfileValidation = editProfileSchemaBase(t)\n\n  return { editProfileValidation }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/model/hooks/use-hydrate-profile.ts",
    "content": "import { useEffect } from 'react'\n\nimport { useMeQuery } from '@/features/auth/api/use-me.query'\n\nimport { useProfileStore } from '../profile-store'\n\nexport const useHydrateProfile = () => {\n  const { data: me, isLoading } = useMeQuery()\n  const hydrateProfileFromStorage = useProfileStore((state) => state.hydrateProfileFromStorage)\n  const resetProfile = useProfileStore((state) => state.resetProfile)\n\n  useEffect(() => {\n    if (isLoading) {\n      return\n    }\n\n    if (!me?.userId) {\n      resetProfile()\n      return\n    }\n\n    hydrateProfileFromStorage(me.userId)\n  }, [hydrateProfileFromStorage, isLoading, me?.userId, resetProfile])\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/model/index.ts",
    "content": "export * from './hooks'\nexport * from './profile-store'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/model/profile-schemas.ts",
    "content": "import type { TFunction } from 'i18next'\n\ntype ValidateField = (value: string) => true | string\n\nconst createValidator =\n  (t: TFunction, field: 'name' | 'surname'): ValidateField =>\n  (value) => {\n    const trimmed = value.trim()\n\n    if (!trimmed) {\n      return t(`profile.title.required_${field}`)\n    }\n\n    if (trimmed.length < 2) {\n      return t(`profile.title.min_value_${field}`, { quantity: '2' })\n    }\n\n    if (trimmed.length > 20) {\n      return t(`profile.title.max_value_${field}`, { quantity: '20' })\n    }\n\n    return true\n  }\n\nexport const editProfileSchemaBase = (t: TFunction) => ({\n  validateName: createValidator(t, 'name'),\n  validateSurname: createValidator(t, 'surname'),\n})\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/model/profile-store.ts",
    "content": "import { create } from 'zustand'\n\nimport { authStorage } from '@/shared/utils/authStorage'\n\nimport { emptyProfile } from '../config/empty-profile'\nimport type { FullName, Profile } from '../types/profile.types'\nimport { profileStorage } from '../utils/profile-storage'\n\ntype ProfileStore = {\n  isEditProfileModalOpen: boolean\n  profile: Profile\n  setEditProfileModalOpen: (isOpen: boolean) => void\n  setProfileAvatar: (avatar: string | null) => void\n  setProfileFullName: (fullName: FullName) => void\n  hydrateProfileFromStorage: (userId?: string) => void\n  resetProfile: () => void\n}\n\nexport const useProfileStore = create<ProfileStore>((set) => ({\n  isEditProfileModalOpen: false,\n  profile: emptyProfile,\n\n  setEditProfileModalOpen: (isOpen) => set({ isEditProfileModalOpen: isOpen }),\n  setProfileAvatar: (avatar) => set((state) => ({ profile: { ...state.profile, avatar } })),\n  setProfileFullName: (fullName) => set((state) => ({ profile: { ...state.profile, fullName } })),\n\n  hydrateProfileFromStorage: (userId) => {\n    const hasToken = !!authStorage.getAccessToken()\n    if (!hasToken || !userId) {\n      set({ profile: emptyProfile })\n      return\n    }\n\n    const stored = profileStorage.getProfile(userId)\n    if (stored) {\n      set({ profile: stored })\n      return\n    }\n\n    set({ profile: emptyProfile })\n  },\n\n  resetProfile: () => set({ profile: emptyProfile, isEditProfileModalOpen: false }),\n}))\n\nexport const selectIsEditProfileModalOpen = (state: ProfileStore) => state.isEditProfileModalOpen\nexport const selectProfileAvatar = (state: ProfileStore) => state.profile.avatar\nexport const selectProfileFullName = (state: ProfileStore) => state.profile.fullName\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/types/index.ts",
    "content": "export * from './profile.types'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/types/profile.types.ts",
    "content": "export type FullName = {\n  name: string\n  surname: string\n}\n\nexport type Profile = {\n  fullName: FullName\n  avatar: string | null\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/ui/EditProfileModal/EditProfileModal.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 745px;\n}\n\n.dialog [data-testid='container'] > * {\n  border-radius: 50%;\n}\n\n.form {\n  overflow-y: auto;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n  margin-bottom: 16px;\n}\n\n.imageUploader {\n  width: 280px;\n  margin: 0 auto;\n}\n\n.imageUploader > label,\n.imageUploader div {\n  overflow: hidden;\n  border-radius: 50%;\n}\n\n.imageUploader button {\n  top: 40px;\n  right: 40px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/ui/EditProfileModal/EditProfileModal.tsx",
    "content": "import { useState } from 'react'\nimport { useForm } from 'react-hook-form'\nimport { useTranslation } from 'react-i18next'\nimport { toast } from 'react-toastify'\n\nimport { useMeQuery } from '@/features/auth/api/use-me.query'\nimport { useEditProfileSchema } from '@/features/profile/model/hooks'\nimport { useProfileStore } from '@/features/profile/model/profile-store'\nimport type { Profile } from '@/features/profile/types/profile.types'\nimport { profileStorage } from '@/features/profile/utils/profile-storage'\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  FormControlledTextField,\n  ImageUploader,\n  Typography,\n} from '@/shared/components'\n\nimport s from './EditProfileModal.module.css'\n\ntype FormData = {\n  name: string\n  surname: string\n}\n\nconst convertFileToBase64 = (file: File): Promise<string> =>\n  new Promise((resolve, reject) => {\n    const reader = new FileReader()\n    reader.onload = () => resolve(reader.result as string)\n    reader.onerror = reject\n    reader.readAsDataURL(file)\n  })\n\nexport const EditProfileModal = () => {\n  const { t } = useTranslation()\n  const { editProfileValidation } = useEditProfileSchema()\n  const { data: me } = useMeQuery()\n\n  const setEditProfileModalOpen = useProfileStore((state) => state.setEditProfileModalOpen)\n  const setProfileAvatar = useProfileStore((state) => state.setProfileAvatar)\n  const setProfileFullName = useProfileStore((state) => state.setProfileFullName)\n  const profileFullName = useProfileStore((state) => state.profile.fullName)\n  const profileAvatarUrl = useProfileStore((state) => state.profile.avatar)\n\n  const [selectedImage, setSelectedImage] = useState<File | null>(null)\n\n  const {\n    control,\n    handleSubmit,\n    formState: { isSubmitting, isValid },\n  } = useForm<FormData>({\n    defaultValues: {\n      name: profileFullName?.name || '',\n      surname: profileFullName?.surname || '',\n    },\n    mode: 'onChange',\n  })\n\n  const handleClose = () => {\n    setEditProfileModalOpen(false)\n  }\n\n  const handleImageSelect = (file: File) => {\n    setSelectedImage(file)\n  }\n\n  const onSubmit = async (data: FormData) => {\n    if (!me?.userId) {\n      toast('Failed to save profile', { type: 'error', theme: 'colored' })\n      return\n    }\n\n    try {\n      const avatarBase64 = selectedImage\n        ? await convertFileToBase64(selectedImage)\n        : profileAvatarUrl\n      const fullName = data\n\n      const profile: Profile = {\n        fullName,\n        avatar: avatarBase64 || null,\n      }\n\n      profileStorage.saveProfile(me.userId, profile)\n      setProfileAvatar(profile.avatar)\n      setProfileFullName(fullName)\n\n      handleClose()\n    } catch {\n      toast('Failed to save profile', { type: 'error', theme: 'colored' })\n    }\n  }\n\n  return (\n    <Dialog open onClose={handleClose} className={s.dialog}>\n      <DialogHeader>\n        <Typography variant=\"h2\">{t('profile.title.edit_profile')}</Typography>\n      </DialogHeader>\n\n      <form onSubmit={handleSubmit(onSubmit)} className={s.form}>\n        <DialogContent className={s.content}>\n          <ImageUploader\n            className={s.imageUploader}\n            onImageSelect={handleImageSelect}\n            initialImageUrl={profileAvatarUrl || undefined}\n            placeholder={t('profile.placeholder.upload_avatar')}\n          />\n\n          <FormControlledTextField\n            control={control}\n            name=\"name\"\n            rules={{ validate: editProfileValidation.validateName }}\n            label={t('profile.label.name')}\n            placeholder={t('profile.placeholder.enter_profile_name')}\n          />\n\n          <FormControlledTextField\n            control={control}\n            name=\"surname\"\n            rules={{ validate: editProfileValidation.validateSurname }}\n            label={t('profile.label.surname')}\n            placeholder={t('profile.placeholder.enter_profile_surname')}\n          />\n        </DialogContent>\n\n        <DialogFooter>\n          <Button variant=\"secondary\" onClick={handleClose} type=\"button\" disabled={isSubmitting}>\n            {t('button.cancel')}\n          </Button>\n          <Button variant=\"primary\" type=\"submit\" disabled={isSubmitting || !isValid}>\n            {isSubmitting ? t('button.saving') : t('button.save_changes')}\n          </Button>\n        </DialogFooter>\n      </form>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/ui/EditProfileModal/index.ts",
    "content": "export { EditProfileModal } from './EditProfileModal'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/ui/index.ts",
    "content": "export * from './EditProfileModal'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/utils/index.ts",
    "content": "export * from './profile-storage'\nexport * from './storage-key'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/utils/profile-storage.ts",
    "content": "import type { Profile } from '../types/profile.types'\nimport { getProfileStorageKey } from './storage-key'\n\nexport const profileStorage = {\n  getProfile(userId: string): Profile | null {\n    const raw = localStorage.getItem(getProfileStorageKey(userId))\n    if (!raw) return null\n\n    try {\n      return JSON.parse(raw) as Profile\n    } catch {\n      return null\n    }\n  },\n\n  saveProfile(userId: string, profile: Profile) {\n    localStorage.setItem(getProfileStorageKey(userId), JSON.stringify(profile))\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/profile/utils/storage-key.ts",
    "content": "export const getProfileStorageKey = (userId: string) => `profile_${userId}`\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/api/index.ts",
    "content": "export * from './tags-api'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/api/tags-api.ts",
    "content": "export const MOCK_HASHTAGS = [\n  'Rock',\n  'Jazz',\n  'Blues',\n  'Metal',\n  'Folk',\n  'Coding',\n  'Dark Ambient',\n  'Chill',\n  'Lo-fi',\n]\n\nexport const MOCK_5_HASHTAGS = MOCK_HASHTAGS.slice(0, 5)\n\nexport type TagDto = {\n  id: string\n  name: string\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/api/use-tags.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\n\nimport type { TagDto } from './tags-api'\n\nexport const useTags = (search?: string) => {\n  return useQuery({\n    queryKey: ['tags', search],\n    queryFn: () => {\n      return getClient().GET('/tags/search', {\n        params: {\n          query: {\n            search: search || '',\n          },\n        },\n      })\n    },\n    select: (response): TagDto[] => {\n      return (\n        response.data?.data?.map((tag) => ({\n          id: tag.id,\n          name: tag.attributes.name,\n        })) || []\n      )\n    },\n    enabled: true,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/index.ts",
    "content": "export type { TagDto } from './api/tags-api'\nexport { useTags } from './api/use-tags.query'\nexport { TagsList } from './ui/TagsList'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/ui/TagsList/TagsList.module.css",
    "content": ".list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/ui/TagsList/TagsList.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_HASHTAGS } from '../../api/tags-api'\nimport { TagsList } from './TagsList'\n\nconst meta: Meta<typeof TagsList> = {\n  title: 'entities/TagsList',\n  component: TagsList,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TagsList>\n\nexport const Default: Story = {\n  args: {\n    tags: MOCK_HASHTAGS.map((name, index) => ({ id: String(index), name })),\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/ui/TagsList/TagsList.tsx",
    "content": "import { Link } from 'react-router'\n\nimport type { TagDto } from '@/features/tags/api/tags-api'\nimport { Tag } from '@/shared/components'\n\nimport s from './TagsList.module.css'\n\nexport const TagsList = ({\n  tags,\n  entity = 'tracks',\n}: {\n  tags: TagDto[]\n  entity?: 'tracks' | 'playlists'\n}) => {\n  return (\n    <ul className={s.list}>\n      {tags.map((tag) => (\n        <li key={tag.id}>\n          <Tag as={Link} to={`/${entity}?tags=${tag.id}`} tag={tag.name} />\n        </li>\n      ))}\n    </ul>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/ui/TagsList/index.ts",
    "content": "export { TagsList } from './TagsList'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tags/ui/index.ts",
    "content": "export { TagsList } from './TagsList'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/api/index.ts",
    "content": "export * from './tracksApi'\nexport * from './types'\nexport * from './use-track-mutations'\nexport * from './use-playlist-tracks.query'\nexport * from './use-track.query'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/api/query-key-factory.ts",
    "content": "import type { SchemaGetTracksRequestPayload } from '@/shared/api/schema'\n\nexport const tracksKeys = {\n  all: ['tracks'] as const, // tracks\n  lists: () => [...tracksKeys.all, 'list'] as const, // tracks, list\n  list: (filters: Partial<SchemaGetTracksRequestPayload> | undefined) =>\n    [...tracksKeys.lists(), filters] as const, // ['tracks', 'list', {pageNumber: 1, pageSize: 20}]\n  infinite: (filters: Partial<SchemaGetTracksRequestPayload> | undefined) =>\n    [...tracksKeys.lists(), 'infinite', filters] as const, // ['tracks', 'list', 'infinite', {search: 'rock'}]\n  details: () => [...tracksKeys.all, 'detail'] as const, // ['tracks', 'detail']\n  detail: (trackId: string) => [...tracksKeys.details(), trackId] as const, // ['tracks','detail','abc123']\n  playlist: (playlistId: string) => [...tracksKeys.all, 'playlist', playlistId] as const, // ['tracks','playlist','abc123']\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/api/tracksApi.ts",
    "content": "import { getClient } from '@/shared/api/client'\nimport { ReactionValue } from '@/shared/api/schema.ts'\n\nexport const tracksApi = {\n  likeTrack: (trackId: string) =>\n    getClient().POST('/playlists/tracks/{trackId}/likes', {\n      params: { path: { trackId } },\n    }),\n\n  dislikeTrack: (trackId: string) =>\n    getClient().POST('/playlists/tracks/{trackId}/dislikes', {\n      params: { path: { trackId } },\n    }),\n\n  removeTrackReaction: (trackId: string) =>\n    getClient().DELETE('/playlists/tracks/{trackId}/reactions', {\n      params: { path: { trackId } },\n    }),\n\n  addTrackToPlaylist: (playlistId: string, trackId: string) =>\n    getClient().POST('/playlists/{playlistId}/relationships/tracks', {\n      params: { path: { playlistId } },\n      body: {\n        data: {\n          type: 'playlist-tracks',\n          attributes: {\n            trackId,\n          },\n        },\n      },\n    }),\n\n  unbindTrackFromPlaylist: (playlistId: string, trackId: string) =>\n    getClient().DELETE('/playlists/{playlistId}/relationships/tracks/{trackId}', {\n      params: { path: { playlistId, trackId } },\n    }),\n\n  removeTrack: (trackId: string) =>\n    getClient().DELETE('/playlists/tracks/{trackId}', {\n      params: { path: { trackId } },\n    }),\n}\n\nexport const MOCK_TRACKS = [\n  {\n    id: '1',\n    type: 'tracks',\n    attributes: {\n      artist: 'Headlund',\n      id: '1',\n      title: 'Days That Matter',\n      addedAt: '2025-06-01T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/110/110',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 104,\n      dislikesCount: 2,\n      artists: [{ id: '1', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '2',\n    type: 'tracks',\n    attributes: {\n      artist: 'Stellar Wave',\n      id: '2',\n      title: 'Cosmic Dust',\n      addedAt: '2025-06-02T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/111/111',\n          },\n        ],\n      },\n      user: {\n        id: '2',\n        name: 'Jane Smith',\n      },\n      currentUserReaction: ReactionValue.Value1,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '2', name: 'Jane Smith' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '3',\n    type: 'tracks',\n    attributes: {\n      artist: 'Aqua Marine',\n      id: '3',\n      title: 'Ocean Breath Is The Best Track Ever',\n      addedAt: '2025-06-03T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/112/112',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 1,\n      dislikesCount: 2,\n      artists: [\n        { id: '3', name: 'Peter Jones' },\n        { id: '4', name: 'Chris Green' },\n        { id: '5', name: 'John Doe' },\n      ],\n      duration: 100,\n    },\n  },\n  {\n    id: '4',\n    type: 'tracks',\n    attributes: {\n      artist: 'Night Rider',\n      id: '4',\n      title: 'Midnight Drive',\n      addedAt: '2025-06-04T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/113/113',\n          },\n        ],\n      },\n      user: {\n        id: '3',\n        name: 'Peter Jones',\n      },\n      currentUserReaction: ReactionValue.ValueMinus1,\n      likesCount: 666,\n      dislikesCount: 2,\n      artists: [{ id: '4', name: 'Chris Green' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '5',\n    type: 'tracks',\n    attributes: {\n      artist: 'Urban Glow',\n      id: '5',\n      title: 'City Lights',\n      addedAt: '2025-06-05T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/114/114',\n          },\n        ],\n      },\n      user: {\n        id: '2',\n        name: 'Jane Smith',\n      },\n      currentUserReaction: ReactionValue.Value1,\n      likesCount: 8,\n      dislikesCount: 2,\n      artists: [{ id: '5', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '6',\n    type: 'tracks',\n    attributes: {\n      artist: 'Whispering Pines',\n      id: '6',\n      title: 'Forest Lullaby',\n      addedAt: '2025-06-06T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/115/115',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 1,\n      dislikesCount: 2,\n      duration: 100,\n    },\n  },\n  {\n    id: '7',\n    type: 'tracks',\n    attributes: {\n      artist: 'Sandstorm',\n      id: '7',\n      title: 'Desert Mirage',\n      addedAt: '2025-06-07T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/116/116',\n          },\n        ],\n      },\n      user: {\n        id: '4',\n        name: 'Susan Lee',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '7', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '8',\n    type: 'tracks',\n    attributes: {\n      artist: 'Altitude',\n      id: '8',\n      title: 'Mountain Peak',\n      addedAt: '2025-06-08T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/117/117',\n          },\n        ],\n      },\n      user: {\n        id: '3',\n        name: 'Peter Jones',\n      },\n      currentUserReaction: ReactionValue.Value1,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '8', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '9',\n    type: 'tracks',\n    attributes: {\n      artist: 'Water Lily',\n      id: '9',\n      title: 'River Flow',\n      addedAt: '2025-06-09T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/118/118',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: ReactionValue.ValueMinus1,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '10', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '10',\n    type: 'tracks',\n    attributes: {\n      artist: 'Galaxy Explorer',\n      id: '10',\n      title: 'Final Frontier',\n      addedAt: '2025-06-10T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/119/119',\n          },\n        ],\n      },\n      user: {\n        id: '5',\n        name: 'Chris Green',\n      },\n      currentUserReaction: ReactionValue.Value0,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '10', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n]\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/api/types.ts",
    "content": "export enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = -1,\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/api/use-playlist-tracks.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { tracksKeys } from './query-key-factory'\nimport { getClient } from '@/shared/api/client'\n\nexport const usePlaylistTracks = (playlistId: string) => {\n  return useQuery({\n    queryKey: tracksKeys.playlist(playlistId),\n    queryFn: async () => {\n      const response = await getClient().GET('/playlists/{playlistId}/tracks', {\n        params: { path: { playlistId } },\n      })\n      return response.data\n    },\n    enabled: !!playlistId,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/api/use-track-mutations.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { tracksApi } from './tracksApi'\nimport { tracksKeys } from './query-key-factory'\nimport { playlistsKeys } from '@/features/playlists/api/query-key-factory'\nimport { getClient } from '@/shared/api/client'\nimport type { SchemaUpdateTrackRequestPayload } from '@/shared/api/schema'\n\nexport const useAddTrackToPlaylistMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: ({ playlistId, trackId }: { playlistId: string; trackId: string }) =>\n      tracksApi.addTrackToPlaylist(playlistId, trackId),\n    onSuccess: (_, { trackId, playlistId }) => {\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n      void queryClient.invalidateQueries({ queryKey: tracksKeys.detail(trackId) })\n    },\n  })\n}\n\nexport const useRemoveTrackFromPlaylistMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: ({ playlistId, trackId }: { playlistId: string; trackId: string }) =>\n      tracksApi.unbindTrackFromPlaylist(playlistId, trackId),\n    onSuccess: (_, { trackId, playlistId }) => {\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n      void queryClient.invalidateQueries({ queryKey: tracksKeys.detail(trackId) })\n    },\n  })\n}\n\nexport const useRemoveTrackMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: (trackId: string) => tracksApi.removeTrack(trackId),\n    onSuccess: () => {\n      void queryClient.invalidateQueries({ queryKey: tracksKeys.all })\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n    },\n  })\n}\n\nexport const usePublishTrackMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: (trackId: string) =>\n      getClient().POST('/playlists/tracks/{trackId}/actions/publish', {\n        params: { path: { trackId } },\n      }),\n    onSuccess: (_, trackId) => {\n      void queryClient.invalidateQueries({ queryKey: tracksKeys.all })\n      void queryClient.invalidateQueries({ queryKey: tracksKeys.detail(trackId) })\n    },\n  })\n}\n\nexport const useUpdateTrackMutation = () => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: ({\n      trackId,\n      payload,\n    }: {\n      trackId: string\n      payload: SchemaUpdateTrackRequestPayload\n    }) =>\n      getClient().PUT('/playlists/tracks/{trackId}', {\n        params: { path: { trackId } },\n        body: payload,\n      }),\n    onSuccess: (_, { trackId }) => {\n      void queryClient.invalidateQueries({ queryKey: tracksKeys.all })\n      void queryClient.invalidateQueries({ queryKey: tracksKeys.detail(trackId) })\n      void queryClient.invalidateQueries({ queryKey: playlistsKeys.all })\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/api/use-track.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { tracksKeys } from './query-key-factory'\nimport { getClient } from '@/shared/api/client'\n\nexport const useTrack = (trackId: string) => {\n  return useQuery({\n    queryKey: tracksKeys.detail(trackId),\n    queryFn: async () => {\n      const response = await getClient().GET('/playlists/tracks/{trackId}', {\n        params: { path: { trackId } },\n      })\n      return response.data\n    },\n    enabled: !!trackId,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/api/use-tracks.query.ts",
    "content": "import { keepPreviousData, useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client'\nimport type { SchemaGetTracksRequestPayload } from '@/shared/api/schema'\nimport { VU } from '@/shared/utils'\n\nimport { tracksKeys } from './query-key-factory'\n\nexport const useTracks = (params: SchemaGetTracksRequestPayload) => {\n  return useQuery({\n    queryKey: tracksKeys.list(params),\n\n    queryFn: () => {\n      return getClient().GET('/playlists/tracks', {\n        params: {\n          query: {\n            ...params,\n            search: params.search || undefined,\n            tagsIds: VU.isValid(params.tagsIds) ? params.tagsIds : undefined,\n            artistsIds: VU.isValid(params.artistsIds) ? params.artistsIds : undefined,\n            userId: params.userId || undefined,\n            cursor: params.cursor || undefined,\n          },\n        },\n      })\n    },\n\n    placeholderData: keepPreviousData,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/model/useTrackReactions.ts",
    "content": "import { useEntityReactions } from '@/shared/hooks/useEntityReactions'\n\nimport { tracksKeys } from '../api/query-key-factory'\nimport { tracksApi } from '../api/tracksApi'\n\nexport const useTrackReactions = (trackId: string) =>\n  useEntityReactions({\n    entityId: trackId,\n    api: {\n      like: tracksApi.likeTrack,\n      dislike: tracksApi.dislikeTrack,\n      remove: tracksApi.removeTrackReaction,\n    },\n    keys: tracksKeys,\n  })\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/CreateTrackForm/CreateTrackModal.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 745px;\n}\n\n.form {\n  overflow-y: auto;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n  margin-bottom: 16px;\n}\n\n.imageUploader {\n  width: 280px;\n  margin: 0 auto;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/CreateTrackForm/CreateTrackModal.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { type SubmitHandler, useForm } from 'react-hook-form'\nimport { useTranslation } from 'react-i18next'\nimport { toast } from 'react-toastify'\n\nimport { useUpdateTrackMutation } from '@/features/tracks/api/use-track-mutations'\nimport { useTrack } from '@/features/tracks/api/use-track.query'\nimport s from '@/features/playlists/ui/CreatePlaylistModal/CreatePlaylistModal.module.css'\nimport { useCreateTrack } from '@/pages/TracksPage/model/useUploadTrack'\nimport { useUploadTrackCover } from '@/pages/TracksPage/model/useUploadTrackCover'\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  ImageUploader,\n  Textarea,\n  TextField,\n  Typography,\n} from '@/shared/components'\nimport { useUIStore } from '@/shared/model/ui-store'\n\ntype UploadTrackData = {\n  title: string\n  file?: FileList\n  lyrics?: string\n}\n\nexport const CreateTrackModal = ({ onClose }: { onClose: () => void }) => {\n  const { t } = useTranslation()\n  const { editingTrackId } = useUIStore()\n  const isEditMode = !!editingTrackId\n  const { mutate } = useCreateTrack()\n  const { mutate: updateTrack } = useUpdateTrackMutation()\n  const { data: trackResponse } = useTrack(editingTrackId || '')\n  const track = trackResponse?.data\n  const trackCoverUrl = track?.attributes.images.main?.[0]?.url\n  const [selectedFile, setSelectedFile] = useState<File | null>(null)\n  const { mutate: mutateUploadCover } = useUploadTrackCover()\n\n  const { register, handleSubmit, reset } = useForm<UploadTrackData>()\n\n  useEffect(() => {\n    if (!isEditMode || !track) {\n      return\n    }\n\n    reset({\n      title: track.attributes.title,\n      lyrics: track.attributes.lyrics || '',\n    })\n  }, [isEditMode, reset, track])\n\n  const onSubmit: SubmitHandler<UploadTrackData> = (data) => {\n    if (isEditMode) {\n      if (!editingTrackId || !track) {\n        return\n      }\n\n      const payload = {\n        data: {\n          type: 'tracks',\n          attributes: {\n            title: data.title,\n            lyrics: data.lyrics || null,\n            releaseDate: track.attributes.releaseDate || null,\n            tagIds: track.attributes.tags.map((tag) => tag.id),\n            artistsIds: track.attributes.artists.map((artist) => artist.id),\n          },\n        },\n      } as const\n\n      updateTrack(\n        { trackId: editingTrackId, payload },\n        {\n          onSuccess: () => {\n            if (selectedFile) {\n              mutateUploadCover(\n                {\n                  trackId: editingTrackId,\n                  cover: selectedFile,\n                },\n                {\n                  onSettled: () => {\n                    onClose()\n                    reset()\n                    setSelectedFile(null)\n                  },\n                }\n              )\n              return\n            }\n\n            toast(t('tracks.success.uploaded_successfully'), {\n              type: 'success',\n              theme: 'colored',\n            })\n            onClose()\n            reset()\n          },\n        }\n      )\n      return\n    }\n\n    if (!data.file || data.file.length === 0) {\n      toast(t('tracks.error.need_select_file'), {\n        type: 'error',\n        theme: 'colored',\n      })\n      return\n    }\n\n    const file = data.file[0]\n    const maxSize = 1024 * 1024\n    const allowedExtensions = ['.mp3', '.MP3']\n    const fileExtension = file!.name.toLowerCase().slice(file!.name.lastIndexOf('.'))\n\n    if (!allowedExtensions.includes(fileExtension)) {\n      toast(t('tracks.error.incorrect_audio_format'), {\n        type: 'error',\n        theme: 'colored',\n      })\n      return\n    }\n\n    if (file!.size > maxSize) {\n      toast(t('tracks.error.file_too_large', { size: Math.round(maxSize / (1024 * 1024)) }), {\n        type: 'error',\n        theme: 'colored',\n      })\n      return\n    }\n\n    mutate(\n      {\n        title: data.title,\n        file: file!,\n      },\n      {\n        onSuccess: (response) => {\n          const trackId = response.id\n\n          if (selectedFile && trackId) {\n            mutateUploadCover(\n              {\n                trackId,\n                cover: selectedFile,\n              },\n              {\n                onSuccess: () => {\n                  onClose()\n                  toast(t('tracks.success.upload_cover'), {\n                    type: 'success',\n                    theme: 'colored',\n                  })\n                  setSelectedFile(null)\n                },\n                onError: () => {\n                  onClose()\n                  toast(t('tracks.error.upload_cover'), {\n                    type: 'error',\n                    theme: 'colored',\n                  })\n                  setSelectedFile(null)\n                },\n              }\n            )\n          }\n\n          toast(t('tracks.success.uploaded_successfully'), {\n            type: 'success',\n            theme: 'colored',\n          })\n          onClose()\n          reset()\n        },\n      }\n    )\n  }\n\n  const handleImageSelect = (fileCover: File) => {\n    setSelectedFile(fileCover)\n  }\n\n  return (\n    <Dialog open onClose={onClose} className={s.dialog}>\n      <DialogHeader>\n        <Typography variant=\"h2\">\n          {isEditMode ? t('tracks.button.edit') : t('tracks.title.create')}\n        </Typography>\n      </DialogHeader>\n\n      <form className={s.form} onSubmit={handleSubmit(onSubmit)}>\n        <DialogContent className={s.content}>\n          {!isEditMode && (\n            <TextField\n              type={'file'}\n              label={t('tracks.label.audio')}\n              placeholder={t('tracks.button.upload')}\n              {...register('file')}\n            />\n          )}\n          <ImageUploader\n            className={s.imageUploader}\n            onImageSelect={handleImageSelect}\n            maxSizeInMB={0.1}\n            enableCrop\n            cropShape=\"rect\"\n            initialImageUrl={isEditMode ? trackCoverUrl : undefined}\n          />\n          <TextField\n            label={t('title.title')}\n            placeholder={t('tracks.placeholder.title')}\n            {...register('title', { required: true })}\n          />\n          <Textarea\n            rows={3}\n            label={t('tracks.label.lyrics')}\n            placeholder={t('tracks.placeholder.lyrics')}\n            {...register('lyrics')}\n          />\n        </DialogContent>\n\n        <DialogFooter>\n          <Button variant=\"secondary\" onClick={onClose} type=\"button\">\n            {t('button.cancel')}\n          </Button>\n          <Button variant=\"primary\" type=\"submit\">\n            {t('button.create')}\n          </Button>\n        </DialogFooter>\n      </form>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackActions/TrackActions.tsx",
    "content": "import { useMemo, useState } from 'react'\n\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { usePlaylists } from '@/features/playlists/api/use-playlists.query'\nimport { ChoosePlaylistModal } from '@/features/playlists/ui/ChoosePlaylistModal/ChoosePlaylistModal'\nimport {\n  useAddTrackToPlaylistMutation,\n  usePublishTrackMutation,\n  useRemoveTrackFromPlaylistMutation,\n  useRemoveTrackMutation,\n} from '@/features/tracks/api/use-track-mutations'\nimport { ReactionButtons, type ReactionButtonsProps } from '@/shared/components'\nimport { useUIStore } from '@/shared/model/ui-store'\n\nimport { syncTrackPlaylists } from '../../utils/playlistSync'\nimport { TrackActionsMenu } from '../TrackActionsMenu/TrackActionsMenu'\n\ntype TrackActionsProps = {\n  trackId: string\n  isOwner?: boolean\n  isPublished?: boolean\n  playlistId?: string\n} & Partial<Omit<ReactionButtonsProps, 'entityId'>>\n\nexport const TrackActions = ({\n  currentReaction,\n  likesCount,\n  onLike,\n  onDislike,\n  onRemoveReaction,\n  trackId,\n  isOwner = false,\n  isPublished,\n  playlistId,\n}: TrackActionsProps) => {\n  const [isOpenChoosePlaylistModal, setIsOpenChoosePlaylistModal] = useState(false)\n\n  const { data: playlistsResponse } = usePlaylists(\n    { trackId },\n    { enabled: isOpenChoosePlaylistModal }\n  )\n\n  const originalPlaylistIds = useMemo(\n    () => playlistsResponse?.data?.data.map((playlist) => playlist.id) ?? [],\n    [playlistsResponse?.data?.data]\n  )\n\n  const [selectedPlaylistIds, setSelectedPlaylistIds] = useState<string[]>([])\n\n  const { data: me } = useMeQuery()\n  const isAuth = !!me\n\n  const { mutateAsync: addTrackToPlaylist } = useAddTrackToPlaylistMutation()\n  const { mutateAsync: removeTrackFromPlaylist } = useRemoveTrackFromPlaylistMutation()\n  const { mutate: removeTrack } = useRemoveTrackMutation()\n  const { mutate: publishTrack } = usePublishTrackMutation()\n  const { openCreateTrackModal } = useUIStore()\n\n  const handleOpenChoosePlaylistModal = () => {\n    setSelectedPlaylistIds(originalPlaylistIds)\n    setIsOpenChoosePlaylistModal(true)\n  }\n\n  const handleDelete = () => {\n    if (playlistId) {\n      removeTrackFromPlaylist({ playlistId, trackId })\n    } else {\n      removeTrack(trackId)\n    }\n  }\n\n  return (\n    <>\n      {currentReaction !== undefined && (\n        <ReactionButtons\n          entityId={trackId}\n          currentReaction={currentReaction}\n          onLike={onLike!}\n          onDislike={onDislike!}\n          likesCount={likesCount!}\n          onRemoveReaction={onRemoveReaction!}\n        />\n      )}\n      {isAuth && (\n        <TrackActionsMenu\n          trackId={trackId}\n          isOwner={isOwner}\n          isPublished={isPublished}\n          onEdit={() => openCreateTrackModal(trackId)}\n          onDelete={handleDelete}\n          onAddToPlaylist={handleOpenChoosePlaylistModal}\n          onPublish={() => publishTrack(trackId)}\n        />\n      )}\n      {isOpenChoosePlaylistModal && (\n        <ChoosePlaylistModal\n          isOpen={isOpenChoosePlaylistModal}\n          setIsOpen={setIsOpenChoosePlaylistModal}\n          playlistIds={selectedPlaylistIds}\n          setPlaylistIds={setSelectedPlaylistIds}\n          onChoose={() => {\n            void syncTrackPlaylists({\n              originalPlaylistIds: originalPlaylistIds,\n              newPlaylistIds: selectedPlaylistIds,\n              trackId,\n              addTrackToPlaylist: (params) => addTrackToPlaylist(params),\n              removeTrackFromPlaylist: (params) => removeTrackFromPlaylist(params),\n            })\n          }}\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackActions/index.ts",
    "content": "export * from './TrackActions'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackActionsMenu/TrackActionsMenu.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { useNavigate } from 'react-router'\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/shared/components'\nimport { useCurrentPage } from '@/shared/hooks/useCurrentPage'\nimport {\n  AddToPlaylistIcon,\n  DeleteIcon,\n  EditIcon,\n  MoreIcon,\n  TextIcon,\n  UploadIcon,\n} from '@/shared/icons'\n\ntype TrackActionsMenuProps = {\n  trackId: string\n  isOwner: boolean\n  isPublished?: boolean\n  onEdit: () => void\n  onDelete: () => void\n  onAddToPlaylist: () => void\n  onPublish?: () => void\n}\n\nexport const TrackActionsMenu = ({\n  trackId,\n  isOwner,\n  isPublished,\n  onEdit,\n  onDelete,\n  onAddToPlaylist,\n  onPublish,\n}: TrackActionsMenuProps) => {\n  const { t } = useTranslation()\n  const { isTrackPage, isPlaylistPage } = useCurrentPage()\n  const navigate = useNavigate()\n\n  const showDelete = !isTrackPage\n  const showLyrics = isTrackPage\n\n  const deleteLabel = isPlaylistPage ? 'tracks.button.delete_from_playlist' : 'tracks.button.delete'\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        {isOwner && (\n          <>\n            <DropdownMenuItem onClick={onEdit}>\n              <EditIcon />\n              {t('tracks.button.edit')}\n            </DropdownMenuItem>\n            {!isPublished && onPublish && (\n              <DropdownMenuItem onClick={onPublish}>\n                <UploadIcon width={24} height={24} />\n                {t('tracks.button.publish')}\n              </DropdownMenuItem>\n            )}\n            {showDelete && (\n              <DropdownMenuItem onClick={onDelete}>\n                <DeleteIcon width={24} height={24} />\n                {t(deleteLabel)}\n              </DropdownMenuItem>\n            )}\n          </>\n        )}\n        <DropdownMenuItem onClick={onAddToPlaylist}>\n          <AddToPlaylistIcon />\n          {t('tracks.button.add_to_playlist')}\n        </DropdownMenuItem>\n        {showLyrics && (\n          <DropdownMenuItem onClick={() => navigate(`/tracks/${trackId}/lyrics`)}>\n            <TextIcon />\n            {t('tracks.button.show_text_song')}\n          </DropdownMenuItem>\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackCard/TrackCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n\n  width: 128px;\n\n  transition: background-color 0.2s;\n}\n\n.image {\n  position: relative;\n  overflow: hidden;\n  height: 103px;\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:has(> .image:hover) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.playback {\n  position: absolute;\n  z-index: 999;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n\n  width: 50%;\n  height: 50%;\n\n  opacity: 0;\n\n  transition: opacity 0.2s;\n}\n\n.image:hover .playback {\n  opacity: 1;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.artists {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  text-overflow: ellipsis;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackCard/TrackCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { TrackCard } from './TrackCard'\n\nconst meta: Meta<typeof TrackCard> = {\n  title: 'entities/TrackCard',\n  component: TrackCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TrackCard>\n\nexport const Default: Story = {\n  args: {\n    id: '1',\n    title: 'Name Song',\n    image: 'https://unsplash.it/182/182',\n    artists: 'Ed Sheeran, Big Sean, Juice W...',\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    id: '1',\n    title: 'A very long track title that should be truncated',\n    image: 'https://unsplash.it/183/183',\n    artists:\n      'A lot of artists on this track, so many that the text should overflow and be truncated by ellipsis',\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackCard/TrackCard.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { Link } from 'react-router'\n\nimport {\n  Card,\n  CoverImage,\n  IconButton,\n  ReactionButtons,\n  type ReactionButtonsProps,\n  Typography,\n} from '@/shared/components'\nimport { PauseIcon, PlayIcon } from '@/shared/icons'\n\nimport s from './TrackCard.module.css'\n\ntype Props = {\n  id: string\n  image: string\n  title: string\n  artists: string\n  onPlaybackClick: () => void\n  isPlaying: boolean\n} & Omit<ReactionButtonsProps, 'className' | 'entityId'>\n\nexport const TrackCard = ({\n  id,\n  image,\n  title,\n  artists,\n  onPlaybackClick,\n  isPlaying,\n  currentReaction,\n  onRemoveReaction,\n  onLike,\n  onDislike,\n  likesCount,\n}: Props) => {\n  const { t } = useTranslation()\n\n  return (\n    <Card className={s.card}>\n      <div className={s.image}>\n        <CoverImage imageSrc={image} imageDescription={title} />\n        <IconButton className={s.playback} onClick={onPlaybackClick}>\n          {isPlaying ? <PauseIcon /> : <PlayIcon />}\n        </IconButton>\n      </div>\n\n      <Typography variant=\"h3\" className={s.title} as={Link} to={`/tracks/${id}`}>\n        {title}\n      </Typography>\n\n      <Typography variant=\"body3\" className={s.artists}>\n        {artists || t('player.unknown_artist')}\n      </Typography>\n      <ReactionButtons\n        currentReaction={currentReaction}\n        entityId={id}\n        likesCount={likesCount}\n        onDislike={onDislike}\n        onLike={onLike}\n        onRemoveReaction={onRemoveReaction}\n      />\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackCard/index.ts",
    "content": "export * from './TrackCard'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackInfoCell/TrackInfoCell.module.css",
    "content": ".box {\n  display: flex;\n  gap: 21px;\n}\n\n.image {\n  position: relative;\n  flex-shrink: 0;\n  width: 52px;\n  height: 52px;\n  cursor: pointer;\n}\n\n.image img {\n  object-fit: cover;\n}\n\n.info {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n\n  max-width: 280px;\n  min-width: 0;\n}\n\n.titleRow {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  min-width: 0;\n}\n\n.draftBadge {\n  flex-shrink: 0;\n  font-size: 11px;\n  line-height: 1;\n  padding: 2px 6px;\n  border-radius: 4px;\n  background-color: var(--color-text-secondary);\n  color: var(--color-bg-primary);\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  font-weight: 600;\n}\n\n.title {\n  color: var(--color-text-main);\n  text-decoration: none;\n\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.title:hover {\n  text-decoration: underline;\n}\n\n.title.playing {\n  color: var(--color-accent);\n}\n\n.artists {\n  color: var(--color-text-secondary);\n\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.playButton {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  background-color: var(--color-bg-card);\n  border: none;\n  padding: 0;\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 0.15s ease;\n}\n\n.boxHovered .playButton {\n  opacity: 1;\n  pointer-events: auto;\n}\n\n.playButton svg {\n  width: var(--font-size-xxl);\n  height: var(--font-size-xxl);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackInfoCell/TrackInfoCell.tsx",
    "content": "import clsx from 'clsx'\nimport { Link } from 'react-router'\nimport { useTranslation } from 'react-i18next'\n\nimport { usePlayerStore } from '@/player/model/player-store.ts'\nimport { CoverImage, IconButton, TableCell, Typography } from '@/shared/components'\nimport { PauseIcon, PlayIcon } from '@/shared/icons'\n\nimport s from './TrackInfoCell.module.css'\n\nexport const TrackInfoCell = ({\n  image,\n  title,\n  artists,\n  isPlaying,\n  isHovered,\n  id,\n  isPublished,\n  onPlayClick,\n}: {\n  image?: string\n  title: string\n  artists: string[]\n  isPlaying: boolean\n  isHovered: boolean\n  id: string\n  isPublished?: boolean\n  onPlayClick?: (trackId: string) => void\n}) => {\n  const { t } = useTranslation()\n  const handlePlayClick = () => {\n    onPlayClick?.(id)\n  }\n\n  return (\n    <TableCell>\n      <div className={clsx(s.box, isHovered && s.boxHovered)}>\n        <div className={s.image} onClick={handlePlayClick}>\n          <CoverImage imageSrc={image} imageDescription={title} />\n          <IconButton\n            aria-label=\"Play track\"\n            className={s.playButton}\n            type=\"button\"\n            onClick={(e) => {\n              e.stopPropagation()\n              handlePlayClick()\n            }}>\n            {isPlaying ? <PauseIcon /> : <PlayIcon />}\n          </IconButton>\n        </div>\n        <div className={s.info}>\n          <div className={s.titleRow}>\n            <Typography\n              variant=\"body1\"\n              as={Link}\n              className={clsx(s.title, isPlaying && s.playing)}\n              to={`/tracks/${id}`}>\n              {title}\n            </Typography>\n            {isPublished === false && (\n              <span className={s.draftBadge}>{t('tracks.button.draft')}</span>\n            )}\n          </div>\n          <Typography className={s.artists} variant=\"body2\">\n            {artists.length > 0 ? artists.join(', ') : t('player.unknown_artist')}\n          </Typography>\n        </div>\n      </div>\n    </TableCell>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackInfoCell/index.ts",
    "content": "export * from './TrackInfoCell.tsx'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackOverview/TrackOverview.module.css",
    "content": ".container {\n  display: flex;\n  gap: 24px;\n  background: transparent;\n}\n\n.imageContainer {\n  flex-shrink: 0;\n  width: 297px;\n  height: 297px;\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  box-shadow: 0 4px 60px rgba(0, 0, 0, 0.5);\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  min-width: 0;\n}\n\n.title {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  margin-bottom: 8px;\n\n  font-size: clamp(var(--font-size-xxl), 8vw, var(--font-size-xxxl));\n  font-weight: 900;\n  line-height: 1;\n  white-space: pre-wrap;\n}\n\n.description {\n  opacity: 0.7;\n}\n\n.info {\n  margin-top: auto;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackOverview/TrackOverview.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_5_HASHTAGS } from '@/features/tags/api/tags-api'\n\nimport { TrackOverview } from './TrackOverview'\n\nconst MOCK_TAGS = MOCK_5_HASHTAGS.map((name, index) => ({ id: String(index), name }))\n\nconst meta: Meta<typeof TrackOverview> = {\n  title: 'entities/TrackOverview',\n  component: TrackOverview,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TrackOverview>\n\nexport const Default: Story = {\n  args: {\n    title: 'Chill Mix',\n    image: 'https://unsplash.it/297/297',\n    addedAt: '2025-01-01',\n    artists: ['Julia Wolf', 'ayokay', 'Khalid'],\n    tags: MOCK_TAGS,\n  },\n}\n\nexport const LongTitle: Story = {\n  args: {\n    title: 'This is a Very Long Track Title That Should Scale Responsively',\n    image: 'https://unsplash.it/299/299',\n    addedAt: '2025-01-01',\n    artists: ['Julia Wolf', 'ayokay', 'Khalid'],\n    tags: MOCK_TAGS,\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackOverview/TrackOverview.tsx",
    "content": "import clsx from 'clsx'\nimport { type ComponentProps } from 'react'\nimport { useTranslation } from 'react-i18next'\n\nimport { type TagDto, TagsList } from '@/features/tags'\nimport noCoverPlaceholder from '@/assets/img/no-cover-placeholder.avif'\nimport { Typography } from '@/shared/components'\n\nimport s from './TrackOverview.module.css'\n\ntype TrackOverviewProps = {\n  title: string\n  image?: string\n  addedAt: string\n  artists?: string[]\n  tags?: TagDto[]\n} & ComponentProps<'div'>\n\nexport const TrackOverview = ({\n  title,\n  image = noCoverPlaceholder,\n  addedAt,\n  tags,\n  className,\n  artists,\n  ...props\n}: TrackOverviewProps) => {\n  const { t } = useTranslation()\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <div className={s.imageContainer}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <div className={s.content}>\n        <TagsList tags={tags || []} entity=\"tracks\" />\n\n        <Typography variant=\"h1\" as=\"h1\" className={s.title}>\n          {title}\n        </Typography>\n\n        <div className={s.info}>\n          <Typography variant=\"body1\">{(artists || []).join(', ')}</Typography>\n          <Typography variant=\"body2\">\n            {`${t('tracks.release')} ${new Date(addedAt).toLocaleDateString()}`}\n          </Typography>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackOverview/index.ts",
    "content": "export * from './TrackOverview'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackRow/TrackRow.module.css",
    "content": ".tableRow {\n  cursor: pointer;\n  user-select: none;\n}\n\n.active {\n  background-color: var(--color-bg-interactive-secondary);\n}\n\n.draft {\n  opacity: 0.6;\n}\n\n.playing {\n  color: var(--color-accent);\n}\n\n.progress {\n  width: 183px;\n}\n\n.actions {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackRow/TrackRow.tsx",
    "content": "import clsx from 'clsx'\nimport * as React from 'react'\n\nimport type { TrackRowData } from '@/features/tracks'\nimport { Progress, TableCell, TableRow, Typography } from '@/shared/components'\nimport { useHover } from '@/shared/hooks'\nimport { LiveWaveIcon } from '@/shared/icons'\n\nimport { TrackInfoCell } from '../TrackInfoCell'\nimport s from './TrackRow.module.css'\n\nexport const TrackRow = <T extends TrackRowData>({\n  trackRow,\n  playingTrackId,\n  playingTrackProgress,\n  renderActionsCell,\n  onPlayClick,\n}: {\n  renderActionsCell: (trackRow: T) => React.ReactNode\n  trackRow: T\n  playingTrackId?: string\n  playingTrackProgress?: number\n  onPlayClick?: (trackId: string) => void\n}) => {\n  const [ref, isHovered] = useHover<HTMLTableRowElement>()\n  const isPlaying = playingTrackId === trackRow.id\n\n  const handleRowClick = (e: React.MouseEvent<HTMLTableRowElement>) => {\n    const target = e.target as HTMLElement\n    if (\n      target.closest('button') ||\n      target.closest('[role=\"button\"]') ||\n      target.closest('svg') ||\n      target.closest('input') ||\n      target.closest('a')\n    ) {\n      return\n    }\n\n    onPlayClick?.(trackRow.id)\n  }\n\n  return (\n    <TableRow\n      ref={ref}\n      onClick={handleRowClick}\n      className={clsx(\n        s.tableRow,\n        isPlaying && s.active,\n        trackRow.isPublished === false && s.draft\n      )}>\n      <TableCell className={clsx(isPlaying && s.playing)}>\n        {isPlaying ? <LiveWaveIcon /> : trackRow.index + 1}\n      </TableCell>\n      <TrackInfoCell\n        id={trackRow.id}\n        image={trackRow.image}\n        title={trackRow.title}\n        artists={trackRow.artists}\n        isPlaying={isPlaying}\n        isHovered={isHovered}\n        isPublished={trackRow.isPublished}\n        onPlayClick={onPlayClick}\n      />\n      <TableCell>\n        {isPlaying && (\n          <Progress\n            key={`${trackRow.id}-${playingTrackProgress}`}\n            className={s.progress}\n            // Todo: add duration in tracksRow component for correct progress bar & duration visibility\n            value={playingTrackProgress ?? 0}\n            max={trackRow.duration}\n          />\n        )}\n      </TableCell>\n      <TableCell>\n        <Typography variant=\"body2\" as=\"time\" dateTime={trackRow.addedAt}>\n          {new Date(trackRow.addedAt).toLocaleDateString()}\n        </Typography>\n      </TableCell>\n      <TableCell>\n        <div className={s.actions}>{renderActionsCell(trackRow)}</div>\n      </TableCell>\n      <TableCell>\n        {/* // Todo: add duration in tracksRow component for correct progress bar & duration visibility */}\n        <Typography variant=\"body2\">{trackRow.duration}</Typography>\n      </TableCell>\n    </TableRow>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackRowContainer/TrackRowContainer.module.css",
    "content": ".actionsCell {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TrackRowContainer/TrackRowContainer.tsx",
    "content": "import { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { TrackRow, TrackActions } from '@/features/tracks'\nimport { useTrackReactions } from '../../model/useTrackReactions'\nimport type { TrackRowData } from '..'\nimport s from './TrackRowContainer.module.css'\n\nexport interface TrackRowContainerProps {\n  trackRow: TrackRowData\n  currentTrack: { id: string } | null\n  currentTime: number\n  onPlayClick: (id: string) => void\n  playlistId?: string\n}\nexport const TrackRowContainer = ({\n  trackRow,\n  currentTrack,\n  currentTime,\n  onPlayClick,\n  playlistId,\n}: TrackRowContainerProps) => {\n  const { handleLike, handleDislike, handleRemoveReaction } = useTrackReactions(trackRow.id)\n\n  const { data: me } = useMeQuery()\n  const currentUserId = me?.userId\n\n  return (\n    <TrackRow\n      trackRow={trackRow}\n      playingTrackId={currentTrack?.id}\n      playingTrackProgress={currentTime}\n      onPlayClick={onPlayClick}\n      renderActionsCell={() => (\n        <div className={s.actionsCell}>\n          <TrackActions\n            trackId={trackRow.id}\n            currentReaction={trackRow.currentUserReaction}\n            likesCount={trackRow.likesCount}\n            onLike={handleLike}\n            onDislike={handleDislike}\n            onRemoveReaction={handleRemoveReaction}\n            isOwner={trackRow.ownerId === currentUserId}\n            isPublished={trackRow.isPublished}\n            playlistId={playlistId}\n          />\n        </div>\n      )}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TracksTable/TrackTable.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport {\n  type CurrentUserReaction,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  ReactionButtons,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport { MOCK_TRACKS } from '../../api'\nimport { TracksTable } from './TracksTable'\n\nconst meta: Meta<typeof TracksTable> = {\n  title: 'entities/TracksTable',\n  component: TracksTable,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TracksTable>\n\ntype ReactionsProps =\n  | {\n      likesCount: number\n      dislikesCount: number\n      currentUserReaction: CurrentUserReaction\n    }\n  | {\n      likesCount?: undefined\n      dislikesCount?: undefined\n      currentUserReaction?: undefined\n    }\n\nexport type TrackRowData = {\n  index: number\n  image: string\n  id: string\n  title: string\n  addedAt: string\n  artists: string[]\n  duration: number\n} & ReactionsProps\n\nexport const Default: Story = {\n  args: {\n    trackRows: MOCK_TRACKS.map((track, index) => ({\n      index: index,\n      id: track.id,\n      title: track.attributes.title,\n      image: track.attributes.images.main[0].url,\n      addedAt: track.attributes.addedAt,\n      artists: track.attributes.artists?.map((artist) => artist.name) || [],\n      isPlaying: false,\n      likesCount: track.attributes.likesCount,\n      dislikesCount: track.attributes.dislikesCount,\n      currentUserReaction: track.attributes.currentUserReaction,\n      duration: track.attributes.duration,\n      ownerId: track.attributes.user.id,\n    })),\n    renderTrackRow: (trackRow) => (\n      <TrackRow\n        trackRow={trackRow}\n        playingTrackId={MOCK_TRACKS[0].id}\n        playingTrackProgress={20}\n        renderActionsCell={() => (\n          <>\n            <ReactionButtons\n              currentReaction={trackRow.currentUserReaction}\n              onRemoveReaction={() => {}}\n              onLike={() => {}}\n              onDislike={() => {}}\n              entityId={''}\n              likesCount={trackRow.likesCount}\n            />\n\n            <DropdownMenu>\n              <DropdownMenuTrigger>\n                <MoreIcon />\n              </DropdownMenuTrigger>\n\n              <DropdownMenuContent>\n                <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                  Add to playlist\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                  Show text song\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </>\n        )}\n      />\n    ),\n  },\n}\n\nexport const WithoutReactions: Story = {\n  args: {\n    trackRows: MOCK_TRACKS.map((track, index) => ({\n      index: index,\n      id: track.id,\n      title: track.attributes.title,\n      image: track.attributes.images.main[0].url,\n      addedAt: track.attributes.addedAt,\n      artists: track.attributes.artists?.map((artist) => artist.name) || [],\n      duration: track.attributes.duration,\n      ownerId: track.attributes.user.id,\n    })),\n    renderTrackRow: (trackRow) => (\n      <TrackRow\n        trackRow={trackRow}\n        playingTrackId={MOCK_TRACKS[0].id}\n        playingTrackProgress={20}\n        renderActionsCell={() => (\n          <div>\n            <DropdownMenu>\n              <DropdownMenuTrigger>\n                <MoreIcon />\n              </DropdownMenuTrigger>\n\n              <DropdownMenuContent>\n                <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                  Add to playlist\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                  Show text song\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        )}\n      />\n    ),\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TracksTable/TracksTable.tsx",
    "content": "import type { ReactNode } from 'react'\n\nimport {\n  type CurrentUserReaction,\n  Table,\n  TableBody,\n  TableHead,\n  TableHeaderCell,\n  TableRow,\n} from '@/shared/components'\nimport { ClockIcon } from '@/shared/icons'\nimport { VU } from '@/shared/utils'\nimport { useTranslation } from 'react-i18next'\n\ntype TableColumn = {\n  title: ReactNode\n  width?: string\n}\n\nexport type TracksTableProps<T extends TrackRowData> = {\n  trackRows: T[]\n  renderTrackRow: (trackRow: T) => ReactNode\n}\n\ntype ReactionsProps =\n  | {\n      likesCount: number\n      dislikesCount: number\n      currentUserReaction: CurrentUserReaction\n    }\n  | {\n      likesCount?: undefined\n      dislikesCount?: undefined\n      currentUserReaction?: undefined\n    }\n\nexport type TrackRowData = {\n  index: number\n  image?: string\n  id: string\n  title: string\n  addedAt: string\n  artists: string[]\n  duration: number\n  ownerId: string\n  isPublished?: boolean\n} & ReactionsProps\n\nexport const TracksTable = <T extends TrackRowData>({\n  trackRows,\n  renderTrackRow,\n}: TracksTableProps<T>) => {\n  const { t } = useTranslation()\n\n  const TABLE_COLUMNS: TableColumn[] = [\n    {\n      title: '#',\n      width: '40px',\n    },\n    {\n      title: t('tracks.table.track'),\n    },\n    {\n      title: '',\n    },\n    {\n      title: t('tracks.table.date_added'),\n      width: '120px',\n    },\n    {\n      title: t('tracks.table.actions'),\n      width: '150px',\n    },\n    {\n      title: <ClockIcon />,\n      width: '60px',\n    },\n  ]\n\n  if (!VU.isNotEmptyArray(trackRows)) {\n    return null\n  }\n\n  return (\n    <Table>\n      <TableHead>\n        <TableRow>\n          {TABLE_COLUMNS.map((column, index) => (\n            <TableHeaderCell key={index} style={{ width: column.width }}>\n              {column.title}\n            </TableHeaderCell>\n          ))}\n        </TableRow>\n      </TableHead>\n      <TableBody>{trackRows.map((trackRow) => renderTrackRow(trackRow))}</TableBody>\n    </Table>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TracksTable/index.ts",
    "content": "export * from './TracksTable'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TracksTableSkeleton/TracksTableSkeleton.tsx",
    "content": "import { Skeleton, Table, TableHead, TableHeaderCell, TableRow } from '@/shared/components'\nimport { ClockIcon } from '@/shared/icons'\nimport { useTranslation } from 'react-i18next'\n\ntype Props = {\n  count?: number\n}\n\nexport const TracksTableSkeleton = ({ count = 5 }: Props) => {\n  const { t } = useTranslation()\n\n  const TABLE_COLUMNS = [\n    { title: '#', width: '40px' },\n    { title: t('tracks.table.track') },\n    { title: '' },\n    { title: t('tracks.table.date_added'), width: '120px' },\n    { title: t('tracks.table.actions'), width: '150px' },\n    { title: <ClockIcon />, width: '60px' },\n  ]\n\n  return (\n    <>\n      <Table>\n        <TableHead>\n          <TableRow>\n            {TABLE_COLUMNS.map((column, index) => (\n              <TableHeaderCell key={index} style={{ width: column.width }}>\n                {column.title}\n              </TableHeaderCell>\n            ))}\n          </TableRow>\n        </TableHead>\n      </Table>\n      {Array.from({ length: count }).map((_el, i) => (\n        <Skeleton height={'70px'} key={i} />\n      ))}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/TracksTableSkeleton/index.ts",
    "content": "export * from './TracksTableSkeleton'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/ui/index.ts",
    "content": "export * from './TrackCard'\nexport * from './TrackOverview'\nexport * from './TrackRow/TrackRow'\nexport * from './TracksTable'\nexport * from './TracksTableSkeleton'\nexport * from './TrackActions'\nexport * from './TrackActionsMenu/TrackActionsMenu'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/features/tracks/utils/playlistSync.ts",
    "content": "type PlaylistSyncParams = {\n  originalPlaylistIds: string[]\n  newPlaylistIds: string[]\n  trackId: string\n  addTrackToPlaylist: (params: { trackId: string; playlistId: string }) => Promise<unknown>\n  removeTrackFromPlaylist: (params: { trackId: string; playlistId: string }) => Promise<unknown>\n}\n\n/**\n * Sync track playlists\n * - Add track to new playlists\n * - Remove track from playlists where it is no longer present\n */\nexport const syncTrackPlaylists = async ({\n  originalPlaylistIds,\n  newPlaylistIds,\n  trackId,\n  addTrackToPlaylist,\n  removeTrackFromPlaylist,\n}: PlaylistSyncParams): Promise<void> => {\n  const promises: Promise<unknown>[] = []\n\n  // Add track to new playlists\n  const playlistsToAdd = newPlaylistIds.filter(\n    (playlistId) => !originalPlaylistIds.includes(playlistId)\n  )\n  for (const playlistId of playlistsToAdd) {\n    promises.push(addTrackToPlaylist({ trackId, playlistId }))\n  }\n\n  // Remove track from playlists where it is no longer present\n  const playlistsToRemove = originalPlaylistIds.filter(\n    (playlistId) => !newPlaylistIds.includes(playlistId)\n  )\n  for (const playlistId of playlistsToRemove) {\n    promises.push(removeTrackFromPlaylist({ trackId, playlistId }))\n  }\n\n  await Promise.all(promises)\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/index.css",
    "content": ":root {\n  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Header/Header.module.css",
    "content": ".header {\n  position: fixed;\n  z-index: 1;\n  right: 0;\n  left: 0;\n\n  display: flex;\n  grid-area: header;\n  align-items: center;\n  justify-content: space-between;\n\n  height: var(--header-height);\n  padding: 0 32px;\n}\n\n.actions {\n  min-width: 135px;\n  height: 40px;\n  display: flex;\n  column-gap: 20px;\n  align-items: center;\n\n  padding-left: 5px;\n  border-radius: 40px;\n\n  background-color: rgb(0 0 0 / 80%);\n  backdrop-filter: blur(2px);\n\n  transition: 0.2s;\n}\n\n.actions:hover {\n  background-color: var(--color-bg-primary);\n}\n\n.actionsSkeleton {\n  width: 100%;\n  height: 100%;\n  background-color: transparent;\n  border-radius: 40px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Header/Header.tsx",
    "content": "import { LoginButtonAndModal, ProfileDropdownMenu } from '@/features/auth'\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { selectProfileAvatar, selectProfileFullName, useProfileStore } from '@/features/profile'\nimport { LanguageSwitcher, Skeleton } from '@/shared/components'\nimport { useLocation } from 'react-router'\n\nimport s from './Header.module.css'\n\nexport const Header = () => {\n  const { data, isLoading } = useMeQuery()\n  const { pathname } = useLocation()\n  const hasColorBg = ['/', '/tracks', '/playlists'].includes(pathname)\n  const profileAvatarUrl = useProfileStore(selectProfileAvatar)\n  const profileFullName = useProfileStore(selectProfileFullName)\n\n  return (\n    <header\n      className={s.header}\n      style={{ backgroundColor: hasColorBg ? 'var(--color-bg-primary)' : '' }}>\n      <div className={s.logo}>Musicfun</div>\n      <div className={s.actions}>\n        <LanguageSwitcher />\n        {isLoading && <Skeleton className={s.actionsSkeleton} />}\n        {!isLoading &&\n          (data ? (\n            <ProfileDropdownMenu\n              avatar={profileAvatarUrl}\n              fullName={profileFullName}\n              userLogin={data.login}\n              id={data.userId}\n            />\n          ) : (\n            <LoginButtonAndModal />\n          ))}\n      </div>\n    </header>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Header/index.ts",
    "content": "export { Header } from './Header'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Layout.module.css",
    "content": ".grid {\n  display: grid;\n  grid-template: 'sidebar main' 1fr / 310px 1fr;\n  height: 100vh;\n}\n\n.grid.playerOpen {\n  grid-template: 'sidebar main' 1fr 'player player' var(--player-height) / 310px 1fr;\n}\n\n.main {\n  overflow-y: auto;\n  grid-area: main;\n  padding-top: var(--header-height);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Layout.tsx",
    "content": "import clsx from 'clsx'\nimport { Outlet } from 'react-router'\n\nimport {\n  EditProfileModal,\n  selectIsEditProfileModalOpen,\n  useHydrateProfile,\n  useProfileStore,\n} from '@/features/profile'\nimport { useCurrentTrack } from '@/player'\nimport { Player } from '@/widgets/Player'\n\nimport { Header } from './Header'\nimport s from './Layout.module.css'\nimport { Sidebar } from './Sidebar'\n\nexport const Layout = () => {\n  const { track: currentTrack } = useCurrentTrack()\n  const isPlayerOpen = !!currentTrack\n  const isEditProfileOpen = useProfileStore(selectIsEditProfileModalOpen)\n\n  useHydrateProfile()\n\n  return (\n    <div className={clsx(s.grid, isPlayerOpen && s.playerOpen)}>\n      <Header />\n      <Sidebar />\n      <main className={s.main}>\n        <Outlet />\n      </main>\n      {isPlayerOpen && <Player />}\n      {isEditProfileOpen && <EditProfileModal />}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Sidebar/MenuLinks/MenuLinks.module.css",
    "content": ".column {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list + .list {\n  padding-top: 20px;\n  border-top: 1px solid var(--color-bg-secondary);\n}\n\n.link {\n  all: unset;\n\n  cursor: pointer;\n\n  display: flex;\n  gap: 16px;\n  align-items: center;\n\n  width: fit-content;\n\n  font-size: var(--font-size-m);\n  font-weight: 700;\n  color: var(--color-text-secondary);\n\n  transition: color 0.2s ease;\n}\n\n.link:hover {\n  color: var(--color-text-primary);\n}\n\n.active {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Sidebar/MenuLinks/MenuLinks.tsx",
    "content": "import clsx from 'clsx'\nimport { useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { NavLink } from 'react-router'\n\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { LoginModal } from '@/features/auth/ui/LoginModal'\nimport { CreatePlaylistModal } from '@/features/playlists'\nimport { CreateTrackModal } from '@/features/tracks/ui/CreateTrackForm/CreateTrackModal'\nimport { HomeIcon, LibraryIcon, PlaylistIcon, TrackIcon, UploadIcon } from '@/shared/icons'\nimport { CreateIcon } from '@/shared/icons/CreateIcon'\n\nimport { useUIStore } from '@/shared/model/ui-store'\n\nimport s from './MenuLinks.module.css'\n\ntype MenuLink = {\n  to: string\n  icon: React.ReactNode\n  label: string\n}\n\ntype MenuButton = {\n  onClick: () => void\n  icon: React.ReactNode\n  label: string\n}\n\nexport const MenuLinks = () => {\n  const { data: user } = useMeQuery()\n  const { t } = useTranslation()\n\n  const {\n    isCreatePlaylistModalOpen,\n    isCreateTrackModalOpen,\n    isAuthModalOpen,\n    openCreatePlaylistModal,\n    closeCreatePlaylistModal,\n    openCreateTrackModal,\n    closeCreateTrackModal,\n    openAuthModal,\n    closeAuthModal,\n  } = useUIStore()\n\n  const createLinks: MenuLink[] = useMemo(\n    () => [\n      {\n        to: '/tracks',\n        icon: <TrackIcon />,\n        label: t('sidebar.all_tracks'),\n      },\n      {\n        to: '/playlists',\n        icon: <PlaylistIcon />,\n        label: t('sidebar.all_playlists'),\n      },\n    ],\n    [t]\n  )\n\n  const actionButtons: MenuButton[] = useMemo(\n    () => [\n      {\n        // todo:task, implement upload track\n        onClick: user ? () => openCreateTrackModal() : () => openAuthModal(),\n        icon: <UploadIcon />,\n        label: t('sidebar.upload_track'),\n      },\n      {\n        // todo:task, implement upload playlist\n        onClick: user ? () => openCreatePlaylistModal() : () => openAuthModal(),\n        icon: <CreateIcon />,\n        label: t('sidebar.create_playlist'),\n      },\n    ],\n    [user, t, openCreateTrackModal, openCreatePlaylistModal, openAuthModal]\n  )\n\n  return (\n    <>\n      <nav className={s.column} aria-label=\"Main navigation\">\n        <ul className={s.list}>\n          <li>\n            <SidebarLink\n              to={'/'}\n              icon={<HomeIcon width={32} height={32} />}\n              label={t('sidebar.home')}\n            />\n          </li>\n          {user ? (\n            <li>\n              <SidebarLink\n                to={`/user/${user.userId}`}\n                icon={<LibraryIcon />}\n                label={t('sidebar.your_library')}\n              />\n            </li>\n          ) : (\n            <li>\n              <SidebarButton\n                onClick={() => openAuthModal()}\n                icon={<LibraryIcon />}\n                label={t('sidebar.your_library')}\n              />\n            </li>\n          )}\n        </ul>\n        <ul className={s.list}>\n          {actionButtons.map((props) => (\n            <li key={props.label}>\n              <SidebarButton {...props} />\n            </li>\n          ))}\n        </ul>\n        <ul className={s.list}>\n          {createLinks.map((props) => (\n            <li key={props.to}>\n              <SidebarLink {...props} />\n            </li>\n          ))}\n        </ul>\n      </nav>\n      {isCreatePlaylistModalOpen && <CreatePlaylistModal onClose={closeCreatePlaylistModal} />}\n      {isCreateTrackModalOpen && <CreateTrackModal onClose={closeCreateTrackModal} />}\n      {isAuthModalOpen && <LoginModal onClose={closeAuthModal} />}\n    </>\n  )\n}\n\nconst SidebarLink = ({ to, icon, label }: MenuLink) => (\n  <NavLink to={to} className={({ isActive }) => clsx(s.link, isActive && s.active)}>\n    {icon}\n    {label}\n  </NavLink>\n)\n\nconst SidebarButton = ({ onClick, icon, label }: MenuButton) => (\n  <button onClick={onClick} className={s.link} type=\"button\">\n    {icon}\n    {label}\n  </button>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Sidebar/MenuLinks/index.ts",
    "content": "export * from './MenuLinks'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Sidebar/Sidebar.module.css",
    "content": ".sidebar {\n  overflow-y: auto;\n  display: flex;\n  grid-area: sidebar;\n  flex-direction: column;\n\n  height: calc(100vh - var(--header-height) - var(--player-height));\n  padding: var(--header-height) 30px 0;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Sidebar/Sidebar.tsx",
    "content": "import { MenuLinks } from './MenuLinks'\nimport s from './Sidebar.module.css'\n\nexport const Sidebar = () => {\n  return (\n    <div className={s.sidebar}>\n      <MenuLinks />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/Sidebar/index.ts",
    "content": "export { Sidebar } from './Sidebar'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/layout/index.ts",
    "content": "export { Layout } from './Layout'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/main.tsx",
    "content": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport App from './App.tsx'\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/MainPage/MainPage.module.css",
    "content": ".mainPage {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n}\n\n.artistsList {\n  --list-gap: 24px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/MainPage/MainPage.tsx",
    "content": "import { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\n\nimport { PlaylistCard, PlaylistCardSkeleton } from '@/entities/playlist'\nimport { usePlaylists } from '@/features/playlists/api/use-playlists.query'\nimport { usePlaylistReactions } from '@/features/playlists/model/usePlaylistReactions'\nimport { TagsList, useTags } from '@/features/tags'\nimport { TrackCard } from '@/features/tracks'\nimport { useTrackReactions } from '@/features/tracks/model/useTrackReactions'\nimport { useTracksQuery } from '@/pages/TracksPage/model/useTracksQuery'\nimport {\n  useCurrentTrack,\n  usePlaybackState,\n  usePlayerControls,\n  usePlayerStore,\n  useQueueControls,\n} from '@/player'\nimport { convertApiTracksToPlayerTracks } from '@/player/utils/convert-api-track-to-player-track'\nimport type { SchemaIncludedArtistOutput, SchemaTrackListItemResource } from '@/shared/api/schema'\nimport {\n  type components,\n  PathsPlaylistsGetParametersQuerySortBy,\n  PathsPlaylistsGetParametersQuerySortDirection,\n} from '@/shared/api/schema'\nimport { ReactionButtons } from '@/shared/components'\nimport { getArtistsByTrack } from '@/shared/utils'\n\nimport { ContentList, PageWrapper } from '../common'\nimport s from './MainPage.module.css'\n\ntype PlaylistListItem = components['schemas']['PlaylistListItemResource']\n\nconst NEW_TRACKS_PLAYLIST_ID = 'new-tracks'\n\nconst PlaylistMainPageCard = ({ playlist }: { playlist: PlaylistListItem }) => {\n  const { handleLike, handleDislike, handleRemoveReaction } = usePlaylistReactions(playlist.id)\n\n  return (\n    <PlaylistCard\n      id={playlist.id}\n      title={playlist.attributes.title}\n      images={playlist.attributes.images}\n      userName={playlist.attributes.user.name}\n      userId={playlist.attributes.user.id}\n      addedAt={playlist.attributes.addedAt}\n      tracksCount={playlist.attributes.tracksCount}\n      shouldShowOwnerName\n      shouldShowCreatedDate\n      footer={\n        <ReactionButtons\n          entityId={playlist.id}\n          currentReaction={playlist.attributes.currentUserReaction}\n          likesCount={playlist.attributes.likesCount}\n          onLike={handleLike}\n          onDislike={handleDislike}\n          onRemoveReaction={handleRemoveReaction}\n        />\n      }\n    />\n  )\n}\n\ntype TrackMainPageCardProps = {\n  track: SchemaTrackListItemResource\n  includedArtists: SchemaIncludedArtistOutput[]\n  isPlaying: boolean\n  onPlaybackClick: (trackId: string) => void\n}\n\nconst TrackMainPageCard = ({\n  track,\n  includedArtists,\n  isPlaying,\n  onPlaybackClick,\n}: TrackMainPageCardProps) => {\n  const { handleLike, handleDislike, handleRemoveReaction } = useTrackReactions(track.id)\n\n  return (\n    <TrackCard\n      id={track.id}\n      image={track.attributes.images.main?.[0]?.url || ''}\n      title={track.attributes.title}\n      artists={getArtistsByTrack(track, includedArtists)}\n      isPlaying={isPlaying}\n      onPlaybackClick={() => onPlaybackClick(track.id)}\n      currentReaction={track.attributes.currentUserReaction}\n      likesCount={track.attributes.likesCount}\n      onLike={handleLike}\n      onDislike={handleDislike}\n      onRemoveReaction={handleRemoveReaction}\n    />\n  )\n}\n\nexport const MainPage = () => {\n  const { t } = useTranslation()\n  const { loadPlaylist } = useQueueControls()\n  const { play, pause, resume } = usePlayerControls()\n  const { trackId: currentTrackId } = useCurrentTrack()\n  const { isPlaying } = usePlaybackState()\n  const currentPlaylistId = usePlayerStore((state) => state.currentPlaylistId)\n\n  const { data: tags = [] } = useTags('')\n\n  const { data: playlistsResponse, isLoading: isPlaylistsLoading } = usePlaylists({\n    pageSize: 10,\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.addedAt,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n  })\n  const playlists = playlistsResponse?.data?.data ?? []\n\n  const { data: tracksResponse } = useTracksQuery({\n    pageSize: 10,\n  })\n  const tracks = tracksResponse?.data ?? []\n  const includedArtists = tracksResponse?.included ?? []\n\n  const playerTracks = useMemo(() => convertApiTracksToPlayerTracks(tracks), [tracks])\n\n  const handleTrackCardPlaybackClick = (trackId: string) => {\n    const isCurrentTrack = currentTrackId === trackId\n\n    if (isCurrentTrack) {\n      if (isPlaying) {\n        pause()\n      } else {\n        resume()\n      }\n      return\n    }\n\n    if (currentPlaylistId !== NEW_TRACKS_PLAYLIST_ID) {\n      const playerTrackIndex = playerTracks.findIndex((track) => track.id === trackId)\n      loadPlaylist(NEW_TRACKS_PLAYLIST_ID, playerTracks, playerTrackIndex)\n    }\n\n    const playerTrack = playerTracks.find((track) => track.id === trackId)\n    if (playerTrack) {\n      play(playerTrack, NEW_TRACKS_PLAYLIST_ID)\n    }\n  }\n\n  return (\n    <PageWrapper className={s.mainPage}>\n      <TagsList tags={tags || []} />\n      <ContentList\n        title={t('playlists.title.new_playlists')}\n        data={playlists}\n        isLoading={isPlaylistsLoading}\n        skeleton={<PlaylistCardSkeleton showReactionButtons />}\n        renderItem={(playlist) => <PlaylistMainPageCard playlist={playlist} />}\n      />\n      <ContentList\n        title={t('tracks.title.new_tracks')}\n        data={tracks}\n        renderItem={(track) => (\n          <TrackMainPageCard\n            track={track}\n            includedArtists={includedArtists}\n            isPlaying={currentTrackId === track.id && isPlaying}\n            onPlaybackClick={handleTrackCardPlaybackClick}\n          />\n        )}\n      />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/MainPage/index.ts",
    "content": "export * from './MainPage'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/PlaylistPage.module.css",
    "content": ".playlistOverview {\n  margin-bottom: 46px;\n}\n\n.playlistToolbar {\n  display: flex;\n  gap: 36px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.errorMessage {\n  text-align: center;\n  font-size: var(--font-size-xxxl);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/PlaylistPage.tsx",
    "content": "import { useParams } from 'react-router'\nimport { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { type ChangeEvent } from 'react'\n\nimport { PlaylistOverview } from '@/features/playlists'\nimport { usePlaylist } from '@/features/playlists/api/use-playlist.query'\nimport { usePlaylistReactions } from '@/features/playlists/model/usePlaylistReactions'\nimport { TracksTable } from '@/features/tracks'\nimport { usePlaylistTracks } from '@/features/tracks/api/use-playlist-tracks.query'\nimport { TrackRowContainer } from '@/features/tracks/ui/TrackRowContainer/TrackRowContainer'\nimport {\n  convertApiTracksToPlayerTracks,\n  useCurrentTrack,\n  usePlaybackProgress,\n  usePlayerControls,\n  useQueueControls,\n} from '@/player'\nimport { useMeQuery } from '@/features/auth/api/use-me.query'\nimport { getArtistsByTrack, VU } from '@/shared/utils'\nimport { Typography } from '@/shared/components'\nimport { usePageSearchParams } from '@/shared/hooks'\nimport { usePageBackgroundColor } from '@/shared/hooks'\n\nimport { PageWithoutHeader, SearchTextField } from '../common'\nimport s from './PlaylistPage.module.css'\nimport { ControlPanel } from './ui/ControlPanel'\nimport { PlaylistPageSkeleton } from './ui/PlaylistPageSkeleton'\n\nexport const PlaylistPage = () => {\n  const { t } = useTranslation()\n  const { id: playlistId } = useParams<{ id: string }>()\n\n  const { data: me } = useMeQuery()\n\n  const {\n    data: playlistResponse,\n    isLoading: isPlaylistLoading,\n    isSuccess: isPlaylistSuccess,\n  } = usePlaylist(playlistId!)\n  const playlist = playlistResponse?.data\n\n  const { data: tracksResponse, isLoading: isTracksLoading } = usePlaylistTracks(playlistId!)\n  const tracks = tracksResponse?.data || []\n  const included = tracksResponse?.included || []\n  const { search, debouncedSearch, handleSearchChange } = usePageSearchParams()\n\n  const { handleLike, handleDislike, handleRemoveReaction } = usePlaylistReactions(playlistId!)\n\n  const { play } = usePlayerControls()\n  const { loadPlaylist } = useQueueControls()\n  const { track: currentTrack } = useCurrentTrack()\n  const { currentTime } = usePlaybackProgress()\n\n  const filteredTracks = useMemo(\n    () =>\n      tracks.filter((track) =>\n        track.attributes.title.toLowerCase().includes(debouncedSearch.toLowerCase())\n      ),\n    [tracks, debouncedSearch]\n  )\n  const playerTracks = useMemo(\n    () => convertApiTracksToPlayerTracks(filteredTracks),\n    [filteredTracks]\n  )\n\n  const handlePlayAll = () => {\n    if (!VU.isNotEmptyArray(playerTracks)) return\n    loadPlaylist(playlistId!, playerTracks, 0)\n    play(playerTracks[0], playlistId!)\n  }\n\n  const handlePlayClick = (trackId: string) => {\n    const trackIndex = playerTracks.findIndex((t) => t.id === trackId)\n    if (trackIndex !== -1) {\n      loadPlaylist(playlistId!, playerTracks, trackIndex)\n      play(playerTracks[trackIndex], playlistId!)\n    }\n  }\n\n  const playlistCover = playlist?.attributes.images.main?.[0]?.url || ''\n  const { dominantColor, canvasRef } = usePageBackgroundColor(playlistCover, isPlaylistSuccess)\n\n  if (isPlaylistLoading || isTracksLoading) {\n    return <PlaylistPageSkeleton />\n  }\n\n  if (!playlist) {\n    return (\n      <PageWithoutHeader>\n        <Typography variant=\"h1\" className={s.errorMessage}>\n          {t('playlists.label.load_error')}\n        </Typography>\n      </PageWithoutHeader>\n    )\n  }\n\n  const isOwnPlaylist = me?.userId === playlist.attributes.user.id\n\n  return (\n    <PageWithoutHeader\n      className={s.playlistPage}\n      backgroundColor={dominantColor || 'var(--color-bg-primary)'}>\n      <canvas ref={canvasRef} style={{ display: 'none' }} />\n      <PlaylistOverview\n        className={s.playlistOverview}\n        title={playlist.attributes.title}\n        image={playlistCover}\n        description={playlist.attributes.description || ''}\n        tags={playlist.attributes.tags}\n        userName={playlist.attributes.user.name}\n        tracksCount={playlist.attributes.tracksCount}\n      />\n      <ControlPanel\n        playlistId={playlistId!}\n        isOwnPlaylist={isOwnPlaylist}\n        currentReaction={playlist.attributes.currentUserReaction}\n        likesCount={playlist.attributes.likesCount}\n        onLike={handleLike}\n        onDislike={handleDislike}\n        onRemoveReaction={handleRemoveReaction}\n        onPlayAll={handlePlayAll}\n      />\n      <div className={s.playlistToolbar}>\n        <SearchTextField\n          placeholder={t('tracks.placeholder.search_tracks')}\n          value={search}\n          onChange={(e: ChangeEvent<HTMLInputElement>) => handleSearchChange(e.target.value)}\n        />\n      </div>\n      <TracksTable\n        trackRows={filteredTracks.map((track, index) => {\n          const attributes = track.attributes as any\n\n          return {\n            index,\n            id: track.id,\n            title: track.attributes.title,\n            image: track.attributes.images.main?.[0]?.url,\n            addedAt: track.attributes.addedAt,\n            artists: getArtistsByTrack(track as any, included as any)\n              .split(', ')\n              .filter(Boolean),\n            // Playlist tracks payload currently does not provide duration.\n            // Keep temporary fallback aligned with RTK project.\n            duration: 100,\n            likesCount: Number(attributes.likesCount ?? 0),\n            dislikesCount: Number(attributes.dislikesCount ?? 0),\n            currentUserReaction: track.attributes.currentUserReaction,\n            ownerId: attributes.user?.id ?? playlist.attributes.user.id,\n            isPublished: attributes.isPublished,\n          }\n        })}\n        renderTrackRow={(trackRow) => (\n          <TrackRowContainer\n            key={trackRow.id}\n            trackRow={trackRow}\n            currentTrack={currentTrack}\n            currentTime={currentTime}\n            onPlayClick={handlePlayClick}\n            playlistId={playlistId}\n          />\n        )}\n      />\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/index.ts",
    "content": "export * from './PlaylistPage'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/ui/ControlPanel/ControlPanel.module.scss",
    "content": ".box {\n  display: flex;\n  gap: 24px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.playButton {\n  width: 80px;\n  height: 80px;\n}\n\n.menuIcon {\n  cursor: pointer;\n  width: 16px;\n  height: 16px;\n}\n\n.deleteItem {\n  color: #eb1616;\n\n  &:hover {\n    color: #ffeaea;\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/ui/ControlPanel/ControlPanel.tsx",
    "content": "import {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  IconButton,\n  ReactionButtons,\n} from '@/shared/components'\nimport { useDeletePlaylistAction } from '@/shared/hooks/useDeletePlaylistAction'\nimport { DeleteIcon, EditIcon, MoreIcon, PlayIcon } from '@/shared/icons'\nimport { useUIStore } from '@/shared/model/ui-store'\nimport { useTranslation } from 'react-i18next'\n\nimport s from './ControlPanel.module.scss'\n\ntype ControlPanelProps = {\n  playlistId: string\n  isOwnPlaylist?: boolean\n  currentReaction?: number\n  likesCount?: number\n  onLike?: () => void\n  onDislike?: () => void\n  onRemoveReaction?: () => void\n  onPlayAll?: () => void\n}\n\nexport const ControlPanel = ({\n  playlistId,\n  isOwnPlaylist,\n  currentReaction = 0,\n  likesCount = 0,\n  onLike,\n  onDislike,\n  onRemoveReaction,\n  onPlayAll,\n}: ControlPanelProps) => {\n  const { t } = useTranslation()\n\n  const handleDeletePlaylist = useDeletePlaylistAction(playlistId)\n  const { openCreatePlaylistModal } = useUIStore()\n\n  return (\n    <div className={s.box}>\n      <IconButton className={s.playButton} onClick={onPlayAll}>\n        <PlayIcon />\n      </IconButton>\n\n      <ReactionButtons\n        onRemoveReaction={onRemoveReaction || (() => {})}\n        currentReaction={currentReaction}\n        onLike={onLike || (() => {})}\n        onDislike={onDislike || (() => {})}\n        size=\"large\"\n        entityId={playlistId}\n        likesCount={likesCount}\n      />\n      {isOwnPlaylist && (\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem onClick={() => openCreatePlaylistModal(playlistId)}>\n              <EditIcon className={s.menuIcon} />\n              <span>{t('button.edit')}</span>\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={handleDeletePlaylist} className={s.deleteItem}>\n              <DeleteIcon className={s.menuIcon} />\n              <span>{t('button.delete')}</span>\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/ui/ControlPanel/index.ts",
    "content": "export * from './ControlPanel'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/ui/PlaylistPageSkeleton/PlaylistPageSkeleton.module.css",
    "content": ".playlistPage {\n  --page-gradient-color: #adbf22;\n}\n\n.playlistOverview {\n  margin-bottom: 46px;\n  display: flex;\n  gap: 24px;\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 40px;\n  min-width: 0;\n}\n\n.playlistToolbar {\n  display: flex;\n  gap: 36px;\n  align-items: center;\n  margin-bottom: 25px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/ui/PlaylistPageSkeleton/PlaylistPageSkeleton.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { TracksTableSkeleton } from '@/features/tracks'\nimport { PageWithoutHeader, SearchTextField } from '@/pages/common'\nimport { Skeleton } from '@/shared/components'\n\nimport s from './PlaylistPageSkeleton.module.css'\n\nconst INFO_LINES = 3\nconst TABLE_ROWS = 5\n\nexport const PlaylistPageSkeleton = () => {\n  const { t } = useTranslation()\n\n  return (\n    <PageWithoutHeader className={s.playlistPage}>\n      <div className={s.playlistOverview}>\n        <Skeleton height=\"300px\" width=\"300px\" />\n        <div className={s.content}>\n          <Skeleton height=\"35px\" width=\"400px\" />\n          <Skeleton width=\"500px\" height=\"55px\" />\n          <div>\n            {Array.from({ length: INFO_LINES }).map((_, index) => (\n              <Skeleton key={index} height=\"25px\" />\n            ))}\n          </div>\n        </div>\n      </div>\n      <div className={s.playlistToolbar}>\n        <SearchTextField placeholder={t('tracks.placeholder.search_tracks')} onChange={() => {}} />\n        <Skeleton width=\"25%\" height=\"60px\" />\n      </div>\n      <TracksTableSkeleton count={TABLE_ROWS} />\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistPage/ui/PlaylistPageSkeleton/index.ts",
    "content": "export * from './PlaylistPageSkeleton'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistsPage/PlaylistsPage.module.css",
    "content": ".title {\n  margin-bottom: 24px;\n}\n\n.pagination {\n  margin-top: 32px;\n}\n\n.controls {\n  margin-bottom: 32px;\n}\n\n.controlsRow {\n  display: flex;\n  gap: 32px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 32px;\n}\n\n.autocomplete {\n  max-width: 513px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistsPage/PlaylistsPage.tsx",
    "content": "import { type ChangeEvent, useCallback, useMemo } from 'react'\nimport { PlaylistItem, PlaylistCardSkeleton } from '@/entities/playlist'\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { usePlaylists } from '@/features/playlists/api/use-playlists.query.ts'\nimport { useTags } from '@/features/tags'\nimport {\n  PathsPlaylistsGetParametersQuerySortBy,\n  PathsPlaylistsGetParametersQuerySortDirection,\n  type SchemaGetPlaylistsRequestPayload,\n} from '@/shared/api/schema.ts'\nimport { Autocomplete, Pagination, Typography } from '@/shared/components'\nimport { usePageSearchParams } from '@/shared/hooks'\nimport { VU } from '@/shared/utils'\nimport { useTranslation } from 'react-i18next'\n\nimport { ContentList, PageWithHeader, SearchTextField, SortSelect } from '../common'\nimport s from './PlaylistsPage.module.css'\nimport type { ISortConfig, SortOption } from './PlaylistsPage.types.ts'\n\nconst PAGE_SIZE = 5\n\nconst sortConfig: Record<SortOption, ISortConfig> = {\n  newest: {\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.addedAt,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n  },\n  oldest: {\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.addedAt,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.asc,\n  },\n  mostLiked: {\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.likesCount,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n  },\n  leastLiked: {\n    sortBy: PathsPlaylistsGetParametersQuerySortBy.likesCount,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.asc,\n  },\n} as const\n\nconst getSortOption = (sortBy: string, sortDirection: string): SortOption => {\n  if (sortBy === PathsPlaylistsGetParametersQuerySortBy.likesCount) {\n    return sortDirection === 'asc' ? 'leastLiked' : 'mostLiked'\n  }\n  return sortDirection === 'asc' ? 'oldest' : 'newest'\n}\n\nexport const PlaylistsPage = () => {\n  const { t } = useTranslation()\n\n  // Intentionally keep raw localStorage presence check in tanstack-query-zustand:\n  // this page only gates the initial me-dependent fetch, not token lifecycle.\n  const hasTokens =\n    !!localStorage.getItem('musicfun-access-token') ||\n    !!localStorage.getItem('musicfun-refresh-token')\n  const { data: me, isPending: isMeLoading } = useMeQuery()\n  const playlistsEnabled = !hasTokens || (!isMeLoading && !!me)\n\n  const {\n    search,\n    debouncedSearch,\n    sortBy,\n    sortDirection,\n    tagsIds,\n    pageNumber,\n    handlePageChange,\n    handleSearchChange,\n    handleSortChange: handleSortUpdate,\n    handleTagsChange,\n  } = usePageSearchParams()\n\n  const sort = getSortOption(sortBy, sortDirection)\n\n  const queryParams = useMemo(\n    () => ({\n      search: debouncedSearch,\n      pageNumber,\n      pageSize: PAGE_SIZE,\n      sortBy: sortBy as PathsPlaylistsGetParametersQuerySortBy,\n      sortDirection: sortDirection as PathsPlaylistsGetParametersQuerySortDirection,\n      tagsIds,\n    }),\n    [debouncedSearch, pageNumber, sortBy, sortDirection, tagsIds]\n  )\n\n  const { data, isPending, isError } = usePlaylists(queryParams, { enabled: playlistsEnabled })\n  const { data: tagsData, isPending: isTagsLoading } = useTags('')\n\n  const onSortChange = useCallback(\n    (event: ChangeEvent<HTMLSelectElement>) => {\n      const value = event.target.value as SortOption\n      const { sortBy, sortDirection } = sortConfig[value]\n      handleSortUpdate(sortBy, sortDirection)\n    },\n    [handleSortUpdate]\n  )\n\n  const onSearchChange = useCallback(\n    (event: ChangeEvent<HTMLInputElement>) => {\n      handleSearchChange(event.target.value)\n    },\n    [handleSearchChange]\n  )\n\n  const tagsOptions = useMemo(\n    () =>\n      tagsData?.map((tag) => ({\n        label: tag.name,\n        value: tag.id,\n      })) || [],\n    [tagsData]\n  )\n\n  const content = useMemo(() => {\n    if (isPending) {\n      return (\n        <ContentList\n          data={[1, 2, 3, 4, 5]}\n          renderItem={() => <PlaylistCardSkeleton showReactionButtons />}\n        />\n      )\n    }\n\n    if (isError) {\n      return <>{t('playlists.label.load_error')}</>\n    }\n\n    if (!VU.isValid(data?.data) || !VU.isNotEmptyArray(data?.data?.data)) {\n      return <>{t('playlists.title.playlists_not_found')}</>\n    }\n\n    return (\n      <ContentList\n        data={data.data.data}\n        renderItem={(playlist) => {\n          return <PlaylistItem playlist={playlist} />\n        }}\n      />\n    )\n  }, [data?.data, isError, isPending, t])\n\n  return (\n    <PageWithHeader>\n      <Typography variant=\"h2\" as=\"h1\" className={s.title}>\n        {t('playlists.title.all_playlists')}\n      </Typography>\n      <div className={s.controls}>\n        <div className={s.controlsRow}>\n          <SearchTextField\n            placeholder={t('playlists.placeholder.search_playlist')}\n            onChange={onSearchChange}\n            value={search}\n          />\n          <SortSelect onChange={onSortChange} value={sort} />\n        </div>\n        <Autocomplete\n          options={tagsOptions}\n          value={tagsIds}\n          onChange={handleTagsChange}\n          label={t('tags.label')}\n          placeholder={isTagsLoading ? t('common.loading_tags') : t('tags.placeholder')}\n          disabled={isTagsLoading}\n          className={s.autocomplete}\n        />\n      </div>\n      {content}\n      <Pagination\n        className={s.pagination}\n        page={pageNumber}\n        pagesCount={data?.data?.meta.pagesCount || 1}\n        onPageChange={handlePageChange}\n      />\n    </PageWithHeader>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistsPage/PlaylistsPage.types.ts",
    "content": "import type { SchemaGetPlaylistsRequestPayload } from '@/shared/api/schema.ts'\n\nexport type SortOption = 'mostLiked' | 'leastLiked' | 'newest' | 'oldest'\n\nexport interface ISortConfig {\n  sortBy: SchemaGetPlaylistsRequestPayload['sortBy']\n  sortDirection: SchemaGetPlaylistsRequestPayload['sortDirection']\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistsPage/index.ts",
    "content": "export * from './PlaylistsPage'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistsPage/model/useCreatePlaylist.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client'\nimport { unwrap } from '@/shared/api/utils/unwrap'\n\nexport function useCreatePlaylist() {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: async (payload: { title: string; description: string | null; tags?: string[] }) => {\n      const res = await unwrap(\n        getClient().POST('/playlists', {\n          body: {\n            data: {\n              type: 'playlists',\n              attributes: {\n                title: payload.title,\n                description: payload.description,\n              },\n            },\n          },\n        })\n      )\n      return res.data\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: ['playlists', 'list'],\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistsPage/model/useDeletePlaylist.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client'\nimport { unwrap } from '@/shared/api/utils/unwrap'\n\nexport function useDeletePlaylist() {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: async (playlistId: string) => {\n      await unwrap(\n        getClient().DELETE('/playlists/{playlistId}', { params: { path: { playlistId } } })\n      )\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: ['playlists', 'list'],\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/PlaylistsPage/model/useUploadPlaylistCover.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { playlistsKeys } from '@/features/playlists/api/query-key-factory.ts'\nimport { getClient } from '@/shared/api/client'\nimport { unwrap } from '@/shared/api/utils/unwrap'\n\nexport function useUploadPlaylistCover() {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: async ({ playlistId, file }: { playlistId: string; file: File }) => {\n      const res = await unwrap(\n        getClient().POST('/playlists/{playlistId}/images/main', {\n          params: { path: { playlistId } },\n          body: {\n            file: file as unknown as string,\n          },\n          bodySerializer: (body) => {\n            const formData = new FormData()\n            formData.append('file', body.file)\n            return formData\n          },\n        })\n      )\n      return res.main\n    },\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({\n        queryKey: playlistsKeys.detail(variables.playlistId),\n      })\n      queryClient.invalidateQueries({\n        queryKey: playlistsKeys.lists(),\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackLyricsPage/TrackLyricsPage.module.css",
    "content": ".trackLyricsPage {\n  position: relative;\n}\n\n.trackTextWrapper {\n  padding-inline: 180px;\n  text-align: center;\n  white-space: wrap;\n}\n\n.button {\n  all: unset;\n\n  cursor: pointer;\n\n  position: absolute;\n  z-index: 2;\n  top: 20px;\n\n  display: flex;\n  gap: 12px;\n  align-items: center;\n\n  font-size: var(--font-size-m);\n  font-weight: 700;\n  color: var(--color-text-secondary);\n\n  transition: color 0.2s ease;\n}\n\n.button:hover {\n  color: var(--color-text-primary);\n}\n\n.trackText {\n  margin: 0;\n  font-size: var(--font-size-xxxxl);\n  font-weight: 900;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackLyricsPage/TrackLyricsPage.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { useNavigate, useParams } from 'react-router'\n\nimport { useTrack } from '@/features/tracks/api/use-track.query'\nimport { PageWithoutHeader } from '@/pages/common'\nimport { usePageBackgroundColor } from '@/shared/hooks'\nimport { ArrowBackIcon } from '@/shared/icons'\nimport { getImageByType } from '@/shared/utils/get-image-by-type'\n\nimport s from './TrackLyricsPage.module.css'\n\nexport const TrackLyricsPage = () => {\n  const { t } = useTranslation()\n  const { id } = useParams()\n  const navigate = useNavigate()\n\n  const { data: trackResponse, isLoading, isSuccess } = useTrack(id!)\n  const track = trackResponse?.data\n\n  const trackCover = track?.attributes.images && getImageByType(track.attributes.images, 'original')\n  const { dominantColor, canvasRef } = usePageBackgroundColor(trackCover?.url, isSuccess)\n\n  const trackText = track?.attributes.lyrics || t('tracks.placeholder.no_lyrics')\n\n  return (\n    <PageWithoutHeader className={s.trackLyricsPage} backgroundColor={dominantColor}>\n      <canvas ref={canvasRef} style={{ display: 'none' }} />\n      {dominantColor && (\n        <>\n          <button\n            type=\"button\"\n            className={s.button}\n            onClick={() => {\n              navigate(-1)\n            }}>\n            <ArrowBackIcon />\n            {t('tracks.button.go_back')}\n          </button>\n\n          <div className={s.trackTextWrapper}>\n            {!isLoading && <p className={s.trackText}>{trackText}</p>}\n          </div>\n        </>\n      )}\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackLyricsPage/index.ts",
    "content": "export * from './TrackLyricsPage'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/TrackPage.module.css",
    "content": ".trackOverview {\n  margin-bottom: 46px;\n}\n\n.title {\n  margin-bottom: 18px;\n}\n\n.search {\n  margin-bottom: 24px;\n}\n\n.errorMessage {\n  text-align: center;\n  font-size: var(--font-size-xxxl);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/TrackPage.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { useParams } from 'react-router'\n\nimport { useMeQuery } from '@/features/auth/api/use-me.query'\nimport { usePlaylists } from '@/features/playlists/api/use-playlists.query'\nimport { PlaylistRow } from '@/features/playlists/ui/PlaylistRow'\nimport { TrackOverview } from '@/features/tracks'\nimport { useTrack } from '@/features/tracks/api/use-track.query'\nimport { convertApiTrackToPlayerTrack, type Track } from '@/player'\nimport { Pagination, Typography } from '@/shared/components'\nimport { usePageBackgroundColor, usePageSearchParams } from '@/shared/hooks'\nimport { getImageByType } from '@/shared/utils/get-image-by-type'\n\nimport { ContentList, PageWithoutHeader, SearchTextField } from '../common'\nimport s from './TrackPage.module.css'\nimport { TrackPageSkeleton } from './ui/TrackPageSkeleton'\nimport { ControlPanel } from './ui/ControlPanel'\n\nexport const TrackPage = () => {\n  const { t } = useTranslation()\n  const { id } = useParams()\n\n  const { data: me } = useMeQuery()\n  const {\n    data: trackResponse,\n    isLoading: isTrackLoading,\n    isSuccess: isTrackSuccess,\n  } = useTrack(id!)\n  const track = trackResponse?.data\n  const isTrackOwner = me?.userId === track?.attributes.user.id\n\n  const { search, pageNumber, handlePageChange, handleSearchChange, debouncedSearch } =\n    usePageSearchParams()\n\n  const { data: playlistsResponse, isLoading: isPlaylistsLoading } = usePlaylists({\n    trackId: id!,\n    pageNumber,\n    pageSize: 4,\n    search: debouncedSearch,\n  })\n  const pagesCount = playlistsResponse?.data?.meta.pagesCount || 1\n\n  const trackCover = track?.attributes.images && getImageByType(track.attributes.images, 'original')\n  const { dominantColor, canvasRef } = usePageBackgroundColor(trackCover?.url, isTrackSuccess)\n\n  if (isTrackLoading || isPlaylistsLoading) {\n    return <TrackPageSkeleton />\n  }\n\n  if (!track) {\n    return (\n      <PageWithoutHeader className={s.trackPage}>\n        <Typography variant=\"h1\" className={s.errorMessage}>\n          {t('tracks.label.load_error')}\n        </Typography>\n      </PageWithoutHeader>\n    )\n  }\n\n  const playerTrack: Track = convertApiTrackToPlayerTrack(track)\n\n  return (\n    <PageWithoutHeader backgroundColor={dominantColor || 'var(--color-bg-primary)'}>\n      <canvas ref={canvasRef} style={{ display: 'none' }} />\n\n      <TrackOverview\n        className={s.trackOverview}\n        title={track.attributes.title}\n        image={trackCover?.url}\n        addedAt={track.attributes.addedAt}\n        artists={track.attributes.artists.map((artist) => artist.name)}\n        tags={track.attributes.tags}\n      />\n\n      <ControlPanel\n        track={playerTrack}\n        trackId={track.id}\n        isOwnTrack={!!isTrackOwner}\n        isPublished={track.attributes.isPublished}\n        currentReaction={track.attributes.currentUserReaction}\n        likesCount={track.attributes.likesCount}\n      />\n\n      <Typography variant=\"h2\" className={s.title}>\n        {t('placeholder.which_playlist')}\n      </Typography>\n      <SearchTextField\n        placeholder={t('playlists.placeholder.search_playlist')}\n        className={s.search}\n        value={search}\n        onChange={(e) => handleSearchChange(e.target.value)}\n      />\n\n      <ContentList\n        layout={'row'}\n        data={playlistsResponse?.data?.data}\n        emptyMessage={t('playlists.title.playlists_not_found')}\n        renderItem={(playlist) => (\n          <PlaylistRow\n            key={playlist.id}\n            id={playlist.id}\n            title={playlist.attributes.title}\n            imageSrc={getImageByType(playlist.attributes.images, 'original')?.url}\n          />\n        )}\n      />\n\n      <Pagination\n        className={s.pagination}\n        page={pageNumber}\n        pagesCount={pagesCount}\n        onPageChange={handlePageChange}\n      />\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/index.ts",
    "content": "export * from './TrackPage'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/ui/ControlPanel/ControlPanel.module.css",
    "content": ".box {\n  display: flex;\n  gap: 24px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.playButton {\n  width: 80px;\n  height: 80px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/ui/ControlPanel/ControlPanel.tsx",
    "content": "import { IconButton } from '@/shared/components'\nimport { PlayIcon } from '@/shared/icons'\nimport { TrackActions } from '@/features/tracks'\nimport { useTrackReactions } from '@/features/tracks/model/useTrackReactions'\nimport { PauseIcon } from '@/shared/icons'\nimport { convertApiTrackToPlayerTrack, type Track } from '@/player'\nimport { useCurrentTrack, usePlaybackState, usePlayerControls } from '@/player'\nimport { getClient } from '@/shared/api/client'\n\nimport s from './ControlPanel.module.css'\n\ntype ControlPanelProps = {\n  track: Track\n  trackId: string\n  isOwnTrack: boolean\n  isPublished: boolean\n  currentReaction: number\n  likesCount: number\n}\n\nexport const ControlPanel = ({\n  track,\n  trackId,\n  isOwnTrack,\n  isPublished,\n  currentReaction,\n  likesCount,\n}: ControlPanelProps) => {\n  const { handleLike, handleDislike, handleRemoveReaction } = useTrackReactions(trackId)\n  const { play, pause, resume } = usePlayerControls()\n  const { track: currentTrack } = useCurrentTrack()\n  const { isPlaying, isPaused } = usePlaybackState()\n\n  const handlePlayClick = async () => {\n    if (currentTrack?.id === track.id) {\n      if (isPlaying) {\n        pause()\n      } else if (isPaused) {\n        resume()\n      } else {\n        play(track)\n      }\n      return\n    }\n\n    try {\n      const response = await getClient().GET('/playlists/tracks/{trackId}', {\n        params: { path: { trackId } },\n      })\n\n      if (response.data?.data) {\n        const fullTrack: Track = convertApiTrackToPlayerTrack(response.data.data)\n        play(fullTrack)\n        return\n      }\n    } catch (error) {\n      console.error('Failed to fetch track:', error)\n    }\n\n    play(track)\n  }\n\n  const isCurrentTrack = currentTrack?.id === track.id\n\n  return (\n    <div className={s.box}>\n      <IconButton className={s.playButton} onClick={handlePlayClick}>\n        {isCurrentTrack && isPlaying ? <PauseIcon /> : <PlayIcon />}\n      </IconButton>\n\n      <TrackActions\n        trackId={trackId}\n        isOwner={isOwnTrack}\n        isPublished={isPublished}\n        size=\"large\"\n        currentReaction={currentReaction}\n        likesCount={likesCount}\n        onLike={handleLike}\n        onDislike={handleDislike}\n        onRemoveReaction={handleRemoveReaction}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/ui/ControlPanel/index.ts",
    "content": "export * from './ControlPanel'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/ui/TrackPageSkeleton/TrackPageSkeleton.module.css",
    "content": ".trackPage {\n  --page-gradient-color: #9a3426;\n}\n\n.trackOverview {\n  margin-bottom: 46px;\n  display: flex;\n  gap: 24px;\n  height: 300px;\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 30px;\n  min-width: 0;\n}\n\n.title {\n  padding-top: 16px;\n  margin-bottom: 18px;\n}\n\n.playlists {\n  padding-top: 40px;\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/ui/TrackPageSkeleton/TrackPageSkeleton.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { PageWithoutHeader, SearchTextField } from '@/pages/common'\nimport { Skeleton, Typography } from '@/shared/components'\n\nimport s from './TrackPageSkeleton.module.css'\n\nconst INFO_LINES = 3\nconst PLAYLIST_ROWS = 4\n\nexport const TrackPageSkeleton = () => {\n  const { t } = useTranslation()\n\n  return (\n    <PageWithoutHeader className={s.trackPage}>\n      <div className={s.trackOverview}>\n        <Skeleton height=\"300px\" width=\"300px\" />\n        <div className={s.content}>\n          <Skeleton height=\"35px\" width=\"400px\" />\n          <Skeleton width=\"500px\" height=\"55px\" />\n          <div>\n            {Array.from({ length: INFO_LINES }).map((_, index) => (\n              <Skeleton key={index} height=\"25px\" />\n            ))}\n          </div>\n          <Skeleton width=\"150px\" height=\"30px\" />\n        </div>\n      </div>\n\n      <Skeleton width=\"300px\" height=\"70px\" />\n\n      <Typography variant=\"h2\" className={s.title}>\n        {t('placeholder.which_playlist')}\n      </Typography>\n\n      <SearchTextField placeholder={t('playlists.placeholder.search_playlist')} />\n\n      <div className={s.playlists}>\n        {Array.from({ length: PLAYLIST_ROWS }).map((_, index) => (\n          <Skeleton key={index} height=\"70px\" width=\"100%\" />\n        ))}\n      </div>\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TrackPage/ui/TrackPageSkeleton/index.ts",
    "content": "export * from './TrackPageSkeleton'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/TracksPage.module.css",
    "content": ".title {\n  margin-bottom: 24px;\n}\n\n.controls {\n  margin-bottom: 32px;\n}\n\n.controlsRow {\n  display: flex;\n  gap: 32px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 32px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/TracksPage.tsx",
    "content": "import * as React from 'react'\nimport { type ChangeEvent } from 'react'\nimport { useOnInView } from 'react-intersection-observer'\n\nimport { useArtists } from '@/features/artists'\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { useTags } from '@/features/tags'\nimport { TracksTable, TracksTableSkeleton } from '@/features/tracks'\nimport { TrackRowContainer } from '@/features/tracks/ui/TrackRowContainer/TrackRowContainer.tsx'\nimport { tracksSortFunction } from '@/pages/TracksPage/TracksSortFunction.ts'\nimport { Autocomplete, Spinner, Typography } from '@/shared/components'\nimport { usePageSearchParams } from '@/shared/hooks'\nimport { VU, getArtistsByTrack } from '@/shared/utils'\nimport { useTranslation } from 'react-i18next'\n\nimport { PageWithHeader, SearchTextField, SortSelect } from '../common'\nimport { useTracksInfinityQuery } from './model/useTracksInfinityQuery.ts'\nimport s from './TracksPage.module.css'\nimport {\n  convertApiTrackToPlayerTrack,\n  convertApiTracksToPlayerTracks,\n  useCurrentTrack,\n  usePlaybackState,\n  usePlaybackProgress,\n  usePlayerControls,\n  useQueueControls,\n} from '@/player'\nimport type { SchemaGetTracksRequestPayload } from '@/shared/api/schema'\nimport { usePlayerStore } from '@/player/model/player-store.ts'\n\nconst PAGE_SIZE = 10\n\nexport const TracksPage = () => {\n  const { t } = useTranslation()\n\n  const {\n    search,\n    debouncedSearch,\n    sortBy: currentSortBy,\n    sortDirection: currentSortDirection,\n    tagsIds: hashtags,\n    artistsIds: artists,\n    handleSearchChange,\n    handleSortChange: handleSortUpdate,\n    handleTagsChange,\n    handleArtistsChange,\n  } = usePageSearchParams()\n\n  const selectedSort = React.useMemo(() => {\n    if (currentSortBy === 'likesCount') {\n      return currentSortDirection === 'asc' ? 'leastLiked' : 'mostLiked'\n    }\n    return currentSortDirection === 'asc' ? 'oldest' : 'newest'\n  }, [currentSortBy, currentSortDirection])\n\n  const { sortBy, sortDirection } = tracksSortFunction(selectedSort)\n\n  const { data, isPending, isError, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } =\n    useTracksInfinityQuery({\n      pageSize: PAGE_SIZE,\n      search: debouncedSearch,\n      sortBy: (currentSortBy as any) || sortBy,\n      sortDirection: (currentSortDirection as any) || sortDirection,\n      tagsIds: hashtags,\n      artistsIds: artists,\n    } as SchemaGetTracksRequestPayload)\n\n  const { data: tagsData, isPending: isTagsLoading } = useTags('')\n  const { data: artistsData, isPending: isArtistsLoading } = useArtists('')\n\n  const { currentTime } = usePlaybackProgress()\n  const { play, pause, resume } = usePlayerControls()\n  const { addToQueue } = useQueueControls()\n  const { track: currentTrack } = useCurrentTrack()\n  const { isPlaying } = usePlaybackState()\n  const currentPlaylistId = usePlayerStore((state) => state.currentPlaylistId)\n  const previousTracksCountRef = React.useRef(0)\n\n  const tracks = React.useMemo(() => {\n    return VU.isNotEmptyArray(data?.pages) ? data.pages.map((page) => page.data).flat() : []\n  }, [data?.pages])\n\n  const included = React.useMemo(() => {\n    return VU.isNotEmptyArray(data?.pages)\n      ? data.pages.map((page) => page.included || []).flat()\n      : []\n  }, [data?.pages])\n\n  const tracksRowsData = React.useMemo(() => {\n    return tracks.map((track, index) => {\n      const attributes = track.attributes as any\n      return {\n        index,\n        id: track.id,\n        title: track.attributes.title,\n        image: track.attributes.images.main?.[0]?.url,\n        addedAt: track.attributes.addedAt,\n        artists: getArtistsByTrack(track as any, included as any)\n          .split(', ')\n          .filter(Boolean),\n        duration: ('duration' in attributes ? attributes.duration : 0) || 0,\n        likesCount: track.attributes.likesCount,\n        dislikesCount: ('dislikesCount' in attributes ? attributes.dislikesCount : 0) || 0,\n        currentUserReaction: track.attributes.currentUserReaction,\n        ownerId: track.attributes.user.id,\n        isPublished: track.attributes.isPublished,\n      }\n    })\n  }, [tracks, included])\n\n  const handleSearchTrack = (e: ChangeEvent<HTMLInputElement>) => {\n    handleSearchChange(e.currentTarget.value)\n  }\n\n  const handleSortTracks = (e: ChangeEvent<HTMLSelectElement>) => {\n    const value = e.currentTarget.value\n    const { sortBy, sortDirection } = tracksSortFunction(value)\n    handleSortUpdate(sortBy, sortDirection)\n  }\n\n  const handleClickPlay = React.useCallback(\n    (trackId: string) => {\n      const track = tracks.find((track) => track.id === trackId)\n\n      if (track) {\n        if (currentTrack?.id === trackId) {\n          if (isPlaying) {\n            pause()\n          } else {\n            resume()\n          }\n          return\n        }\n\n        const playerTrack = convertApiTrackToPlayerTrack(track)\n        const playerTracks = convertApiTracksToPlayerTracks(tracks)\n        play(playerTrack, 'all-tracks', playerTracks)\n      }\n    },\n    [currentTrack?.id, isPlaying, pause, play, resume, tracks]\n  )\n\n  React.useEffect(() => {\n    if (!tracks.length) {\n      previousTracksCountRef.current = 0\n      return\n    }\n\n    const previousCount = previousTracksCountRef.current\n    previousTracksCountRef.current = tracks.length\n\n    if (currentPlaylistId !== 'all-tracks' || tracks.length <= previousCount) {\n      return\n    }\n\n    if (previousCount === 0) {\n      return\n    }\n\n    const newTracks = tracks.slice(previousCount)\n    if (newTracks.length > 0) {\n      addToQueue(convertApiTracksToPlayerTracks(newTracks))\n    }\n  }, [addToQueue, currentPlaylistId, tracks])\n\n  const targetRef = useOnInView(\n    (inView: boolean) => {\n      if (inView && hasNextPage && !isFetchingNextPage && !isFetching) {\n        void fetchNextPage()\n      }\n    },\n    {\n      threshold: 0.1,\n      rootMargin: '300px',\n      triggerOnce: false,\n    }\n  )\n\n  if (isError) {\n    return <div>{t('tracks.label.load_error')}</div>\n  }\n\n  const tagsOptions = React.useMemo(\n    () =>\n      tagsData?.map((tag) => ({\n        label: tag.name,\n        value: tag.id,\n      })) || [],\n    [tagsData]\n  )\n\n  const artistsOptions = React.useMemo(\n    () =>\n      artistsData?.map((artist) => ({\n        label: artist.name,\n        value: artist.id,\n      })) || [],\n    [artistsData]\n  )\n\n  return (\n    <PageWithHeader>\n      <Typography variant=\"h2\" as=\"h1\" className={s.title}>\n        {t('tracks.title.all_tracks')}\n      </Typography>\n      <div className={s.controls}>\n        <div className={s.controlsRow}>\n          <SearchTextField\n            placeholder={t('tracks.placeholder.search_tracks')}\n            onChange={handleSearchTrack}\n            value={search}\n          />\n          <SortSelect onChange={handleSortTracks} value={selectedSort} />\n        </div>\n        <div className={s.controlsRow}>\n          <Autocomplete\n            options={tagsOptions}\n            value={hashtags}\n            onChange={handleTagsChange}\n            label={t('tags.label')}\n            placeholder={isTagsLoading ? t('common.loading_tags') : t('tags.placeholder')}\n            disabled={isTagsLoading}\n            className={s.autocomplete}\n          />\n          <Autocomplete\n            options={artistsOptions}\n            value={artists}\n            onChange={handleArtistsChange}\n            label={t('artists.label')}\n            placeholder={isArtistsLoading ? t('common.loading_artists') : t('artists.placeholder')}\n            disabled={isArtistsLoading}\n            className={s.autocomplete}\n          />\n        </div>\n      </div>\n      <div>\n        {isPending ? (\n          <TracksTableSkeleton />\n        ) : (\n          <TracksTable\n            trackRows={tracksRowsData}\n            renderTrackRow={(trackRow) => {\n              return (\n                <TrackRowContainer\n                  key={trackRow.id}\n                  trackRow={trackRow}\n                  currentTrack={currentTrack}\n                  currentTime={currentTime}\n                  onPlayClick={handleClickPlay}\n                />\n              )\n            }}\n          />\n        )}\n\n        {!isPending && tracks.length === 0 && <div>{t('tracks.title.tracks_not_found')}</div>}\n        {hasNextPage && (\n          <div ref={targetRef}>\n            {isFetchingNextPage ? <Spinner size={50} /> : <div style={{ height: '10px' }} />}\n          </div>\n        )}\n      </div>\n    </PageWithHeader>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/TracksSortFunction.ts",
    "content": "import type { SortTracksParams } from '@/pages/TracksPage/tracksPageTypes/TracksPageTypes.ts'\nimport {\n  PathsPlaylistsGetParametersQuerySortDirection,\n  PathsPlaylistsTracksGetParametersQuerySortBy,\n} from '@/shared/api/schema.ts'\n\nexport const tracksSortFunction = (sortValue: string): SortTracksParams => {\n  switch (sortValue) {\n    case 'newest':\n      return {\n        sortBy: PathsPlaylistsTracksGetParametersQuerySortBy.publishedAt,\n        sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n      }\n    case 'oldest':\n      return {\n        sortBy: PathsPlaylistsTracksGetParametersQuerySortBy.publishedAt,\n        sortDirection: PathsPlaylistsGetParametersQuerySortDirection.asc,\n      }\n    case 'mostLiked':\n      return {\n        sortBy: PathsPlaylistsTracksGetParametersQuerySortBy.likesCount,\n        sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n      }\n    case 'leastLiked':\n      return {\n        sortBy: PathsPlaylistsTracksGetParametersQuerySortBy.likesCount,\n        sortDirection: PathsPlaylistsGetParametersQuerySortDirection.asc,\n      }\n    default:\n      return {\n        sortBy: PathsPlaylistsTracksGetParametersQuerySortBy.publishedAt,\n        sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n      }\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/index.ts",
    "content": "export * from './TracksPage'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/model/useTrackDetails.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client'\nimport { unwrap } from '@/shared/api/utils/unwrap'\n\nexport function useTrackDetails(trackId: string) {\n  return useQuery({\n    queryFn: () =>\n      unwrap(\n        getClient().GET('/playlists/tracks/{trackId}', {\n          params: {\n            path: { trackId },\n          },\n        })\n      ),\n    queryKey: ['tracks', 'details', trackId],\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/model/useTracksInfinityQuery.ts",
    "content": "import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'\n\nimport { tracksKeys } from '@/features/tracks/api/query-key-factory'\nimport { getClient } from '@/shared/api/client.ts'\nimport {\n  PathsPlaylistsTracksGetParametersQueryPaginationType,\n  type SchemaGetTracksRequestPayload,\n} from '@/shared/api/schema.ts'\nimport { unwrap } from '@/shared/api/utils/unwrap.ts'\nimport type { Strict } from '@/shared/types/strict.tsx'\n\ntype TracksParams = Partial<SchemaGetTracksRequestPayload>\n\nexport function useTracksInfinityQuery<P extends TracksParams>(\n  params: Strict<TracksParams, P>,\n  opts?: { enabled?: boolean }\n) {\n  return useInfiniteQuery({\n    queryFn: ({ pageParam }) =>\n      unwrap(\n        getClient().GET('/playlists/tracks', {\n          params: {\n            query: {\n              ...params,\n              paginationType: PathsPlaylistsTracksGetParametersQueryPaginationType.cursor,\n              cursor: pageParam,\n            },\n          },\n        })\n      ),\n    queryKey: tracksKeys.infinite(params),\n    initialPageParam: '0',\n    getNextPageParam: (lastPage) => lastPage.meta.nextCursor,\n    placeholderData: keepPreviousData,\n    enabled: opts?.enabled ?? true,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/model/useTracksQuery.tsx",
    "content": "import { keepPreviousData, useQuery } from '@tanstack/react-query'\n\nimport { tracksKeys } from '@/features/tracks/api/query-key-factory'\nimport { getClient } from '@/shared/api/client.ts'\nimport type { SchemaGetTracksRequestPayload } from '@/shared/api/schema.ts'\nimport { unwrap } from '@/shared/api/utils/unwrap.ts'\nimport type { Strict } from '@/shared/types/strict.tsx'\n\ntype TracksParams = Partial<SchemaGetTracksRequestPayload>\n\nexport function useTracksQuery<P extends TracksParams>(params: Strict<TracksParams, P>) {\n  return useQuery({\n    queryFn: () =>\n      unwrap(\n        getClient().GET('/playlists/tracks', {\n          params: {\n            query: params,\n          },\n        })\n      ),\n    queryKey: tracksKeys.list(params),\n    placeholderData: keepPreviousData,\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/model/useUploadTrack.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client'\nimport { unwrap } from '@/shared/api/utils/unwrap'\n\nexport const useCreateTrack = () => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: async ({ title, file }: { title: string; file: File }) => {\n      const formData = new FormData()\n      formData.append('data[type]', 'tracks')\n      formData.append('data[attributes][title]', title)\n      formData.append('file', file)\n\n      const res = await unwrap(\n        getClient().POST('/playlists/tracks/upload', {\n          // FIXME: temporary typescript fix\n          body: formData as unknown as { title: string; file: string },\n        })\n      )\n      return res.data\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: ['tracks', 'list'],\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/model/useUploadTrackCover.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client'\nimport { unwrap } from '@/shared/api/utils/unwrap'\n\nexport const useUploadTrackCover = () => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: async ({ trackId, cover }: { trackId: string; cover: File }) => {\n      const formData = new FormData()\n      formData.append('cover', cover)\n\n      const res = await unwrap(\n        getClient().POST('/playlists/tracks/{trackId}/cover', {\n          params: { path: { trackId } },\n          // FIXME: temporary typescript fix\n          body: formData as unknown as { cover: string },\n        })\n      )\n      return res.main\n    },\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({\n        queryKey: ['tracks', 'detail', variables.trackId],\n      })\n      queryClient.invalidateQueries({\n        queryKey: ['tracks', 'list'],\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/TracksPage/tracksPageTypes/TracksPageTypes.ts",
    "content": "import {\n  PathsPlaylistsGetParametersQuerySortDirection,\n  type PathsPlaylistsTracksGetParametersQuerySortBy,\n} from '@/shared/api/schema.ts'\n\nexport type SortField = PathsPlaylistsTracksGetParametersQuerySortBy\nexport type SortOrder = PathsPlaylistsGetParametersQuerySortDirection\n\nexport type SortTracksParams = {\n  sortBy: SortField\n  sortDirection: SortOrder\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/UserPage.module.css",
    "content": ".userPage {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/UserPage.tsx",
    "content": "import { PageWithoutHeader } from '../common'\nimport { useUserPageBackgroundColor } from './hooks'\nimport { UserInfo, UserTabs } from './ui'\nimport s from './UserPage.module.css'\n\nexport const UserPage = () => {\n  const { dominantColor, canvasRef } = useUserPageBackgroundColor()\n\n  return (\n    <PageWithoutHeader\n      className={s.userPage}\n      backgroundColor={dominantColor || 'var(--color-bg-primary)'}>\n      <canvas ref={canvasRef} style={{ display: 'none' }} />\n      <UserInfo />\n      <UserTabs />\n    </PageWithoutHeader>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/hooks/index.ts",
    "content": "export * from './useUserPageData'\nexport * from './useUserPageBackgroundColor'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/hooks/useUserPageBackgroundColor.ts",
    "content": "import { useMemo } from 'react'\n\nimport { selectProfileAvatar, useProfileStore } from '@/features/profile'\nimport { usePageBackgroundColor } from '@/shared/hooks'\nimport { decodeFileFromBase64 } from '@/shared/utils'\n\nimport { useUserPageData } from './useUserPageData'\n\nexport const useUserPageBackgroundColor = () => {\n  const { isMeQuerySuccess, isProfileOwner } = useUserPageData()\n  const profileAvatarUrl = useProfileStore(selectProfileAvatar)\n\n  const decodedProfileAvatarUrl = useMemo(\n    () => decodeFileFromBase64(profileAvatarUrl),\n    [profileAvatarUrl]\n  )\n  const imageUrlForBackgroundColor = isProfileOwner ? decodedProfileAvatarUrl : null\n  const isLocalUrlData = !!decodedProfileAvatarUrl\n\n  return usePageBackgroundColor(imageUrlForBackgroundColor, isMeQuerySuccess, isLocalUrlData)\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/hooks/useUserPageData.ts",
    "content": "import { useParams } from 'react-router'\nimport { useMeQuery } from '@/features/auth/api/use-me.query'\nimport { usePlaylists } from '@/features/playlists/api/use-playlists.query'\nimport { useTracks } from '@/features/tracks/api/use-tracks.query'\nimport {\n  PathsPlaylistsGetParametersQuerySortBy,\n  PathsPlaylistsGetParametersQuerySortDirection,\n  PathsPlaylistsTracksGetParametersQuerySortBy,\n  PathsPlaylistsTracksGetParametersQueryPaginationType,\n  type SchemaGetTracksRequestPayload,\n} from '@/shared/api/schema'\n\nexport const useUserPageData = () => {\n  const { id: userId } = useParams<{ id: string }>()\n  const { data: me, isLoading: isMeLoading } = useMeQuery()\n  const isProfileOwner = me?.userId === userId\n\n  const { data: playlistsResponse, isLoading: isPlaylistsLoading } = usePlaylists({\n    userId: userId,\n    pageSize: 1, // Just to get totalCount and first item for login hack\n  })\n\n  const { data: tracksResponse, isLoading: isTracksLoading } = useTracks({\n    userId: userId,\n    pageNumber: 1,\n    pageSize: 1,\n    sortBy: PathsPlaylistsTracksGetParametersQuerySortBy.publishedAt,\n    sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n    includeDrafts: isProfileOwner,\n    paginationType: PathsPlaylistsTracksGetParametersQueryPaginationType.offset,\n  } as SchemaGetTracksRequestPayload)\n\n  let userLogin = isProfileOwner ? me?.login : ''\n\n  if (!isProfileOwner && playlistsResponse?.data?.data?.[0]) {\n    userLogin = playlistsResponse.data.data[0].attributes.user.name\n  }\n\n  if (!isProfileOwner && !userLogin && tracksResponse?.data?.data?.[0]) {\n    userLogin = tracksResponse.data.data[0].attributes.user.name\n  }\n\n  return {\n    userId,\n    pageOwnerId: userId,\n    isProfileOwner,\n    userLogin,\n    playlistsCount: playlistsResponse?.data?.meta.totalCount || 0,\n    tracksCount: tracksResponse?.data?.meta.totalCount || 0,\n    isInitialLoading: isMeLoading || isPlaylistsLoading || isTracksLoading,\n    isContentLoading: isPlaylistsLoading || isTracksLoading || isMeLoading,\n    isMeQuerySuccess: !isMeLoading,\n    me,\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/index.ts",
    "content": "export * from './UserPage'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserInfo/UserInfo.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  padding-top: 16px;\n}\n\n.avatar {\n  overflow: hidden;\n  width: 96px;\n  height: 96px;\n  border-radius: 50%;\n}\n\n.userName {\n  margin-top: 8px;\n}\n\n.editButton {\n  margin-top: 6px;\n  border-radius: 4px;\n  font-size: 14px;\n}\n\n.stats {\n  margin-top: 10px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserInfo/UserInfo.tsx",
    "content": "import { Avatar, Button, Typography } from '@/shared/components'\nimport { EditIcon } from '@/shared/icons'\nimport { useTranslation } from 'react-i18next'\nimport {\n  selectProfileAvatar,\n  selectProfileFullName,\n  useEditProfileModal,\n  useProfileStore,\n} from '@/features/profile'\nimport { useUserPageData } from '../../hooks'\nimport { UserInfoSkeleton } from './UserInfoSkeleton'\nimport { UserStats } from './UserStats'\n\nimport s from './UserInfo.module.css'\n\nexport const UserInfo = () => {\n  const { t } = useTranslation()\n  const { isProfileOwner, userLogin, playlistsCount, tracksCount, isInitialLoading } =\n    useUserPageData()\n  const { handleOpenEditProfileModal } = useEditProfileModal()\n  const profileAvatarUrl = useProfileStore(selectProfileAvatar)\n  const profileFullName = useProfileStore(selectProfileFullName)\n\n  const userFullName =\n    isProfileOwner && profileFullName.name\n      ? `${profileFullName.name} ${profileFullName.surname}`\n      : userLogin\n\n  if (isInitialLoading) {\n    return <UserInfoSkeleton />\n  }\n\n  return (\n    <div className={s.box}>\n      <Avatar\n        className={s.avatar}\n        src={isProfileOwner ? profileAvatarUrl : undefined}\n        fullName={isProfileOwner ? profileFullName : undefined}\n        userLogin={userLogin}\n      />\n      <Typography variant=\"h2\" className={s.userName}>\n        {userFullName}\n      </Typography>\n\n      {isProfileOwner && (\n        <Button className={s.editButton} variant=\"secondary\" onClick={handleOpenEditProfileModal}>\n          <EditIcon />\n          {t('button.edit_profile')}\n        </Button>\n      )}\n      <div className={s.stats}>\n        <UserStats playlistsCount={playlistsCount} tracksCount={tracksCount} />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserInfo/UserInfoSkeleton.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 10px;\n  padding-top: 16px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserInfo/UserInfoSkeleton.tsx",
    "content": "import { Skeleton } from '@/shared/components'\n\nimport s from './UserInfoSkeleton.module.css'\n\nexport const UserInfoSkeleton = () => {\n  return (\n    <div className={s.box}>\n      <Skeleton circle={true} width=\"96px\" height=\"96px\" />\n      <Skeleton height=\"30px\" width=\"180px\" />\n      <Skeleton height=\"34px\" width=\"140px\" />\n      <Skeleton height=\"40px\" width=\"190px\" />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserInfo/UserStats.module.css",
    "content": ".descriptionList {\n  display: flex;\n  gap: 28px;\n  margin: 0;\n}\n\n.descriptionItem {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.descriptionItem dd {\n  margin: 0;\n}\n\n.descriptionItem dt {\n  font-size: var(--font-size-s);\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserInfo/UserStats.tsx",
    "content": "import { useTranslation } from 'react-i18next'\n\nimport { Typography } from '@/shared/components'\n\nimport s from './UserStats.module.css'\n\ntype UserStatsProps = {\n  playlistsCount?: number\n  tracksCount?: number\n}\n\nexport const UserStats = ({ playlistsCount = 0, tracksCount = 0 }: UserStatsProps) => {\n  const { t } = useTranslation()\n\n  return (\n    <dl className={s.descriptionList}>\n      <div className={s.descriptionItem}>\n        <Typography as=\"dd\" variant=\"body1\">\n          {playlistsCount}\n        </Typography>\n        <Typography as=\"dt\" variant=\"body2\">\n          {t('profile.stats.playlists', { count: playlistsCount })}\n        </Typography>\n      </div>\n      <div className={s.descriptionItem}>\n        <Typography as=\"dd\" variant=\"body1\">\n          {tracksCount}\n        </Typography>\n        <Typography as=\"dt\" variant=\"body2\">\n          {t('profile.stats.tracks', { count: tracksCount })}\n        </Typography>\n      </div>\n    </dl>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserInfo/index.ts",
    "content": "export * from './UserInfo'\nexport * from './UserStats'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/LikedTracksTab/LikedTracksTab.module.css",
    "content": ""
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/LikedTracksTab/LikedTracksTab.tsx",
    "content": "import { useCallback, useMemo } from 'react'\nimport { useParams, useSearchParams } from 'react-router'\nimport { useTranslation } from 'react-i18next'\n\nimport { TracksTable } from '@/features/tracks'\nimport { useTracks } from '@/features/tracks/api/use-tracks.query'\nimport { TrackRowContainer } from '@/features/tracks/ui/TrackRowContainer/TrackRowContainer'\nimport {\n  PathsPlaylistsGetParametersQuerySortDirection,\n  PathsPlaylistsTracksGetParametersQueryPaginationType,\n  PathsPlaylistsTracksGetParametersQuerySortBy,\n} from '@/shared/api/schema'\nimport { Pagination } from '@/shared/components'\nimport {\n  convertApiTrackToPlayerTrack,\n  convertApiTracksToPlayerTracks,\n  useCurrentTrack,\n  usePlaybackProgress,\n  usePlaybackState,\n  usePlayerControls,\n} from '@/player'\nimport { getArtistsByTrack } from '@/shared/utils'\nimport { usePlayerStore } from '@/player/model/player-store.ts'\nimport { useUserPageData } from '../../../hooks'\n\nconst PAGE_SIZE = 5\nconst DEFAULT_PAGE = 1\n\nexport const LikedTracksTab = () => {\n  const { t } = useTranslation()\n  const { id: userId } = useParams<{ id: string }>()\n  const [searchParams, setSearchParams] = useSearchParams()\n  const { isProfileOwner } = useUserPageData()\n\n  const pageNumber = Number(searchParams.get('page')) || DEFAULT_PAGE\n\n  const { track: currentTrack } = useCurrentTrack()\n  const { currentTime } = usePlaybackProgress()\n  const { isPlaying } = usePlaybackState()\n  const { play, pause, resume } = usePlayerControls()\n  const currentPlaylistId = usePlayerStore((state) => state.currentPlaylistId)\n\n  const queryParams = useMemo(\n    () => ({\n      pageNumber,\n      pageSize: PAGE_SIZE,\n      sortBy: PathsPlaylistsTracksGetParametersQuerySortBy.publishedAt,\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n      userId: userId,\n      includeDrafts: isProfileOwner,\n      paginationType: PathsPlaylistsTracksGetParametersQueryPaginationType.offset,\n    }),\n    [isProfileOwner, pageNumber, userId]\n  )\n\n  const { data, isLoading, isError } = useTracks(queryParams)\n  const tracks = data?.data?.data ?? []\n  const included = data?.data?.included ?? []\n  const totalPages = data?.data?.meta.pagesCount ?? 1\n\n  const handlePageChange = useCallback(\n    (page: number) => {\n      setSearchParams((prev) => {\n        const next = new URLSearchParams(prev)\n\n        if (page === DEFAULT_PAGE) {\n          next.delete('page')\n        } else {\n          next.set('page', page.toString())\n        }\n\n        return next\n      })\n    },\n    [setSearchParams]\n  )\n\n  const playerTracks = useMemo(() => convertApiTracksToPlayerTracks(tracks), [tracks])\n  const likedTracksPlaylistId = `${userId || 'unknown'}-liked-tracks`\n\n  const handlePlayTrack = useCallback(\n    (trackId: string) => {\n      const track = tracks.find((item) => item.id === trackId)\n      if (!track) return\n\n      if (currentTrack?.id === trackId) {\n        if (isPlaying) {\n          pause()\n        } else {\n          resume()\n        }\n        return\n      }\n\n      const playerTrack = convertApiTrackToPlayerTrack(track)\n      if (currentPlaylistId !== likedTracksPlaylistId) {\n        play(playerTrack, likedTracksPlaylistId, playerTracks)\n        return\n      }\n\n      play(playerTrack, likedTracksPlaylistId)\n    },\n    [\n      currentPlaylistId,\n      currentTrack?.id,\n      isPlaying,\n      likedTracksPlaylistId,\n      pause,\n      play,\n      playerTracks,\n      resume,\n      tracks,\n    ]\n  )\n\n  if (isLoading) return null\n  if (isError) return <div>{t('tracks.label.load_error')}</div>\n\n  return (\n    <>\n      <TracksTable\n        trackRows={tracks.map((track, index) => ({\n          index,\n          id: track.id,\n          title: track.attributes.title,\n          image: track.attributes.images.main?.[0]?.url,\n          addedAt: track.attributes.addedAt,\n          artists: getArtistsByTrack(track as any, included as any).split(', '),\n          duration: Number((track.attributes as any).duration ?? 0),\n          likesCount: track.attributes.likesCount,\n          dislikesCount: Number((track.attributes as any).dislikesCount ?? 0),\n          currentUserReaction: track.attributes.currentUserReaction,\n          ownerId: track.attributes.user.id,\n          isPublished: track.attributes.isPublished,\n        }))}\n        renderTrackRow={(trackRow) => (\n          <TrackRowContainer\n            key={trackRow.id}\n            trackRow={trackRow}\n            currentTrack={currentTrack}\n            currentTime={currentTime}\n            onPlayClick={handlePlayTrack}\n          />\n        )}\n      />\n      <Pagination\n        page={pageNumber}\n        pagesCount={Math.max(1, totalPages)}\n        onPageChange={handlePageChange}\n        alwaysVisible\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/LikedTracksTab/index.ts",
    "content": "export * from './LikedTracksTab'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/MyLikedPlaylistsTab.module.css",
    "content": ".playlistsList {\n  display: grid;\n  grid-template-columns: repeat(5, minmax(0, 1fr));\n  gap: 8px;\n}\n\n.playlistsList > li {\n  min-width: 0;\n}\n\n.playlistsList > li > * {\n  width: 100% !important;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/MyLikedPlaylistsTab.tsx",
    "content": "import { useCallback, useMemo } from 'react'\nimport { useParams, useSearchParams } from 'react-router'\nimport { useTranslation } from 'react-i18next'\n\nimport { PlaylistCard } from '@/entities/playlist'\nimport { usePlaylists } from '@/features/playlists/api/use-playlists.query'\nimport { usePlaylistReactions } from '@/features/playlists/model/usePlaylistReactions'\nimport { ContentList } from '@/pages/common'\nimport { Pagination, ReactionButtons } from '@/shared/components'\nimport {\n  type components,\n  PathsPlaylistsGetParametersQuerySortBy,\n  PathsPlaylistsGetParametersQuerySortDirection,\n} from '@/shared/api/schema'\nimport s from './MyLikedPlaylistsTab.module.css'\n\nconst PAGE_SIZE = 5\nconst DEFAULT_PAGE = 1\ntype PlaylistListItem = components['schemas']['PlaylistListItemResource']\n\nconst LikedPlaylistCard = ({ playlist }: { playlist: PlaylistListItem }) => {\n  const { handleLike, handleDislike, handleRemoveReaction } = usePlaylistReactions(playlist.id)\n\n  return (\n    <PlaylistCard\n      id={playlist.id}\n      title={playlist.attributes.title}\n      images={playlist.attributes.images}\n      userName={playlist.attributes.user.name}\n      userId={playlist.attributes.user.id}\n      addedAt={playlist.attributes.addedAt}\n      tracksCount={playlist.attributes.tracksCount}\n      shouldShowOwnerName\n      shouldShowCreatedDate\n      footer={\n        <ReactionButtons\n          entityId={playlist.id}\n          currentReaction={playlist.attributes.currentUserReaction}\n          likesCount={playlist.attributes.likesCount}\n          onLike={handleLike}\n          onDislike={handleDislike}\n          onRemoveReaction={handleRemoveReaction}\n        />\n      }\n    />\n  )\n}\n\nexport const MyLikedPlaylistsTab = () => {\n  const { t } = useTranslation()\n  const { id: userId } = useParams<{ id: string }>()\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const pageNumber = Number(searchParams.get('page')) || DEFAULT_PAGE\n\n  const queryParams = useMemo(\n    () => ({\n      pageNumber,\n      pageSize: PAGE_SIZE,\n      sortBy: PathsPlaylistsGetParametersQuerySortBy.addedAt,\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n      userId: userId,\n    }),\n    [pageNumber, userId]\n  )\n\n  const { data, isLoading, isError } = usePlaylists(queryParams)\n  const playlists = data?.data?.data ?? []\n  const totalPages = data?.data?.meta.pagesCount ?? 1\n\n  const handlePageChange = useCallback(\n    (page: number) => {\n      setSearchParams((prev) => {\n        const next = new URLSearchParams(prev)\n\n        if (page === DEFAULT_PAGE) {\n          next.delete('page')\n        } else {\n          next.set('page', page.toString())\n        }\n\n        return next\n      })\n    },\n    [setSearchParams]\n  )\n\n  if (isLoading) return null\n  if (isError) return <div>Failed to load playlists</div>\n\n  return (\n    <>\n      <ContentList\n        data={playlists}\n        listClassName={s.playlistsList}\n        renderItem={(playlist) => <LikedPlaylistCard playlist={playlist} />}\n      />\n      <Pagination\n        page={pageNumber}\n        pagesCount={Math.max(1, totalPages)}\n        onPageChange={handlePageChange}\n        alwaysVisible\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/index.ts",
    "content": "export * from './MyLikedPlaylistsTab'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/PlaylistsTab/PlaylistsTab.module.css",
    "content": ".createPlaylistButton {\n  display: block;\n\n  width: 328px;\n  height: 54px;\n  margin: 0 auto;\n  margin-bottom: 24px;\n}\n\n.emptyState {\n  text-align: center;\n  padding: 60px 0;\n  color: #888;\n  font-size: 16px;\n}\n\n.playlistsList {\n  display: grid;\n  grid-template-columns: repeat(5, minmax(0, 1fr));\n  gap: 8px;\n}\n\n.playlistsList > li {\n  min-width: 0;\n}\n\n.playlistsList > li > * {\n  width: 100% !important;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/PlaylistsTab/PlaylistsTab.tsx",
    "content": "import { useCallback, useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { useParams, useSearchParams } from 'react-router'\n\nimport { PlaylistCard } from '@/entities/playlist'\nimport { CreatePlaylistModal } from '@/features/playlists'\nimport { usePlaylists } from '@/features/playlists/api/use-playlists.query'\nimport { ContentList } from '@/pages/common'\nimport {\n  PathsPlaylistsGetParametersQuerySortBy,\n  PathsPlaylistsGetParametersQuerySortDirection,\n} from '@/shared/api/schema'\nimport { Button, Pagination } from '@/shared/components'\nimport { useUIStore } from '@/shared/model/ui-store'\nimport { useUserPageData } from '../../../hooks'\n\nimport s from './PlaylistsTab.module.css'\n\nconst PAGE_SIZE = 8\nconst DEFAULT_PAGE = 1\n\nexport const PlaylistsTab = () => {\n  const { t } = useTranslation()\n  const { id: userId } = useParams<{ id: string }>()\n  const { isProfileOwner } = useUserPageData()\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const { isCreatePlaylistModalOpen, openCreatePlaylistModal, closeCreatePlaylistModal } =\n    useUIStore()\n\n  const pageNumber = Number(searchParams.get('page')) || DEFAULT_PAGE\n\n  const queryParams = useMemo(\n    () => ({\n      pageNumber,\n      pageSize: PAGE_SIZE,\n      sortBy: PathsPlaylistsGetParametersQuerySortBy.addedAt,\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n      userId: userId || undefined,\n    }),\n    [pageNumber, userId]\n  )\n  const { data, isLoading, isError } = usePlaylists(queryParams)\n  const playlists = data?.data?.data ?? []\n  const totalPages = data?.data?.meta.pagesCount ?? 1\n\n  const handlePageChange = useCallback(\n    (page: number) => {\n      setSearchParams((prev) => {\n        const next = new URLSearchParams(prev)\n\n        if (page === DEFAULT_PAGE) {\n          next.delete('page')\n        } else {\n          next.set('page', page.toString())\n        }\n\n        return next\n      })\n    },\n    [setSearchParams]\n  )\n\n  return (\n    <>\n      {isProfileOwner && (\n        <Button className={s.createPlaylistButton} onClick={() => openCreatePlaylistModal()}>\n          {t('playlists.button.create_playlist')}\n        </Button>\n      )}\n\n      {isCreatePlaylistModalOpen && <CreatePlaylistModal onClose={closeCreatePlaylistModal} />}\n\n      {isError && <div>Failed to load playlists</div>}\n\n      {!isLoading && !isError && playlists.length > 0 && (\n        <ContentList\n          data={playlists}\n          listClassName={s.playlistsList}\n          renderItem={(playlist) => (\n            <PlaylistCard\n              canEdit={isProfileOwner}\n              id={playlist.id}\n              images={playlist.attributes.images || { main: [] }}\n              key={playlist.id}\n              title={playlist.attributes.title}\n              tracksCount={playlist.attributes.tracksCount}\n            />\n          )}\n        />\n      )}\n      {!isLoading && !isError && playlists.length === 0 && (\n        <div className={s.emptyState}>{t('playlists.title.no_playlists')}</div>\n      )}\n\n      {!isLoading && !isError && (\n        <Pagination\n          page={pageNumber}\n          pagesCount={Math.max(1, totalPages)}\n          onPageChange={handlePageChange}\n          alwaysVisible\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/PlaylistsTab/index.ts",
    "content": "export * from './PlaylistsTab'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/TracksTab/TracksTab.module.css",
    "content": ".uploadTrackButton {\n  display: block;\n\n  width: 328px;\n  height: 54px;\n  margin: 0 auto;\n  margin-bottom: 24px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/TracksTab/TracksTab.tsx",
    "content": "import { useCallback, useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { useParams, useSearchParams } from 'react-router'\n\nimport { TracksTable } from '@/features/tracks'\nimport { useTracks } from '@/features/tracks/api/use-tracks.query.ts'\nimport { CreateTrackModal } from '@/features/tracks/ui/CreateTrackForm/CreateTrackModal'\nimport { TrackRowContainer } from '@/features/tracks/ui/TrackRowContainer/TrackRowContainer'\nimport {\n  PathsPlaylistsGetParametersQuerySortDirection,\n  PathsPlaylistsTracksGetParametersQueryPaginationType,\n  PathsPlaylistsTracksGetParametersQuerySortBy,\n  type SchemaGetTracksRequestPayload,\n} from '@/shared/api/schema.ts'\nimport { Button, Pagination } from '@/shared/components'\nimport { useUIStore } from '@/shared/model/ui-store'\nimport { useUserPageData } from '../../../hooks'\nimport { useCurrentTrack, usePlaybackProgress } from '@/player'\nimport { usePlaybackState, usePlayerControls } from '@/player'\nimport { getArtistsByTrack } from '@/shared/utils'\nimport { convertApiTrackToPlayerTrack, convertApiTracksToPlayerTracks } from '@/player'\nimport { usePlayerStore } from '@/player/model/player-store.ts'\n\nimport s from './TracksTab.module.css'\n\nconst PAGE_SIZE = 5\nconst DEFAULT_PAGE = 1\n\nexport const TracksTab = () => {\n  const { t } = useTranslation()\n  const { id: userId } = useParams<{ id: string }>()\n  const { isProfileOwner } = useUserPageData()\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const { isCreateTrackModalOpen, openCreateTrackModal, closeCreateTrackModal } = useUIStore()\n\n  const pageNumber = Number(searchParams.get('page')) || DEFAULT_PAGE\n\n  const { track: currentTrack } = useCurrentTrack()\n  const { currentTime } = usePlaybackProgress()\n  const { isPlaying } = usePlaybackState()\n  const { play, pause, resume } = usePlayerControls()\n  const currentPlaylistId = usePlayerStore((state) => state.currentPlaylistId)\n\n  const queryParams = useMemo<SchemaGetTracksRequestPayload>(\n    () => ({\n      pageNumber,\n      pageSize: PAGE_SIZE,\n      sortBy: PathsPlaylistsTracksGetParametersQuerySortBy.publishedAt,\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection.desc,\n      userId: userId || undefined,\n      includeDrafts: isProfileOwner,\n      paginationType: PathsPlaylistsTracksGetParametersQueryPaginationType.offset,\n    }),\n    [pageNumber, userId, isProfileOwner]\n  )\n\n  const { data, isLoading, isError } = useTracks(queryParams)\n  const tracks = data?.data?.data ?? []\n  const included = data?.data?.included ?? []\n  const totalPages = data?.data?.meta.pagesCount ?? 1\n\n  const handlePageChange = useCallback(\n    (page: number) => {\n      setSearchParams((prev) => {\n        const next = new URLSearchParams(prev)\n\n        if (page === DEFAULT_PAGE) {\n          next.delete('page')\n        } else {\n          next.set('page', page.toString())\n        }\n\n        return next\n      })\n    },\n    [setSearchParams]\n  )\n\n  const playerTracks = useMemo(() => convertApiTracksToPlayerTracks(tracks), [tracks])\n  const userTracksPlaylistId = `${userId || 'unknown'}-user-tracks`\n\n  const handlePlayTrack = useCallback(\n    (trackId: string) => {\n      const track = tracks.find((item) => item.id === trackId)\n      if (!track) return\n\n      if (currentTrack?.id === trackId) {\n        if (isPlaying) {\n          pause()\n        } else {\n          resume()\n        }\n        return\n      }\n\n      const playerTrack = convertApiTrackToPlayerTrack(track)\n      if (currentPlaylistId !== userTracksPlaylistId) {\n        play(playerTrack, userTracksPlaylistId, playerTracks)\n        return\n      }\n\n      play(playerTrack, userTracksPlaylistId)\n    },\n    [\n      currentPlaylistId,\n      currentTrack?.id,\n      isPlaying,\n      pause,\n      play,\n      playerTracks,\n      resume,\n      tracks,\n      userTracksPlaylistId,\n    ]\n  )\n\n  return (\n    <>\n      {isProfileOwner && (\n        <Button className={s.uploadTrackButton} onClick={() => openCreateTrackModal()}>\n          {t('tracks.button.upload_track')}\n        </Button>\n      )}\n\n      {isCreateTrackModalOpen && <CreateTrackModal onClose={closeCreateTrackModal} />}\n\n      {isError && <div>{t('tracks.label.load_error')}</div>}\n      {!isLoading && !isError && tracks.length > 0 && (\n        <TracksTable\n          trackRows={tracks.map((track, index) => ({\n            index,\n            id: track.id,\n            title: track.attributes.title,\n            image: track.attributes.images.main?.[0]?.url,\n            addedAt: track.attributes.addedAt,\n            artists: getArtistsByTrack(track as any, included as any).split(', '),\n            duration: Number((track.attributes as any).duration ?? 0),\n            likesCount: track.attributes.likesCount,\n            dislikesCount: Number((track.attributes as any).dislikesCount ?? 0),\n            currentUserReaction: track.attributes.currentUserReaction,\n            ownerId: track.attributes.user.id,\n            isPublished: track.attributes.isPublished,\n          }))}\n          renderTrackRow={(trackRow) => (\n            <TrackRowContainer\n              key={trackRow.id}\n              trackRow={trackRow}\n              currentTrack={currentTrack}\n              currentTime={currentTime}\n              onPlayClick={handlePlayTrack}\n            />\n          )}\n        />\n      )}\n\n      {!isLoading && !isError && (\n        <Pagination\n          page={pageNumber}\n          pagesCount={Math.max(1, totalPages)}\n          onPageChange={handlePageChange}\n          alwaysVisible\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/TracksTab/index.ts",
    "content": ""
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/UserTabs.tsx",
    "content": "import { useEffect } from 'react'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/components'\nimport { useTranslation } from 'react-i18next'\nimport { useSearchParams } from 'react-router'\nimport { useUserPageData } from '../../hooks'\n\nimport { LikedTracksTab } from './LikedTracksTab'\nimport { MyLikedPlaylistsTab } from './MyLikedPlaylistsTab'\nimport { PlaylistsTab } from './PlaylistsTab'\nimport { TracksTab } from './TracksTab/TracksTab'\nimport { UserTabsSkeleton } from './UserTabsSkeleton'\n\nexport const UserTabs = () => {\n  const { t } = useTranslation()\n  const { isProfileOwner, userLogin, isContentLoading } = useUserPageData()\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const tabFromUrl = searchParams.get('tab') || 'playlists'\n  const allowedTabs = isProfileOwner\n    ? ['playlists', 'tracks', 'liked-playlists', 'liked-tracks']\n    : ['playlists', 'tracks']\n  const activeTab = allowedTabs.includes(tabFromUrl) ? tabFromUrl : 'playlists'\n\n  useEffect(() => {\n    if (tabFromUrl === activeTab) {\n      return\n    }\n\n    setSearchParams((prev) => {\n      const next = new URLSearchParams(prev)\n      next.delete('page')\n\n      if (activeTab === 'playlists') {\n        next.delete('tab')\n      } else {\n        next.set('tab', activeTab)\n      }\n\n      return next\n    })\n  }, [activeTab, setSearchParams, tabFromUrl])\n\n  const handleTabChange = (value: string) => {\n    setSearchParams((prev) => {\n      const next = new URLSearchParams(prev)\n\n      next.delete('page')\n\n      if (value === 'playlists') {\n        next.delete('tab')\n      } else {\n        next.set('tab', value)\n      }\n\n      return next\n    })\n  }\n\n  if (isContentLoading) {\n    return <UserTabsSkeleton />\n  }\n\n  return (\n    <Tabs value={activeTab} onValueChange={handleTabChange}>\n      <TabsList>\n        <TabsTrigger value=\"playlists\">\n          {t('tabs.playlists')}\n          {!isProfileOwner && userLogin && ` ${userLogin}${t('tabs.possessive_case')}`}\n        </TabsTrigger>\n        <TabsTrigger value=\"tracks\">\n          {t('tabs.tracks')}\n          {!isProfileOwner && userLogin && ` ${userLogin}${t('tabs.possessive_case')}`}\n        </TabsTrigger>\n        {isProfileOwner && (\n          <>\n            <TabsTrigger value=\"liked-playlists\">{t('tabs.liked_playlists')}</TabsTrigger>\n            <TabsTrigger value=\"liked-tracks\">{t('tabs.liked_tracks')}</TabsTrigger>\n          </>\n        )}\n      </TabsList>\n      <TabsContent value=\"playlists\">\n        <PlaylistsTab />\n      </TabsContent>\n      <TabsContent value=\"tracks\">\n        <TracksTab />\n      </TabsContent>\n      {isProfileOwner && (\n        <>\n          <TabsContent value=\"liked-playlists\">\n            <MyLikedPlaylistsTab />\n          </TabsContent>\n          <TabsContent value=\"liked-tracks\">\n            <LikedTracksTab />\n          </TabsContent>\n        </>\n      )}\n    </Tabs>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/UserTabsSkeleton/UserTabsSkeleton.module.css",
    "content": ".tabs {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 32px;\n}\n\n.playlistsTab {\n  align-self: self-start;\n  display: flex;\n  gap: 8px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/UserTabsSkeleton/UserTabsSkeleton.tsx",
    "content": "import { PlaylistCardSkeleton } from '@/entities/playlist'\nimport { Skeleton } from '@/shared/components'\n\nimport s from './UserTabsSkeleton.module.css'\n\nexport const UserTabsSkeleton = () => {\n  return (\n    <div className={s.tabs}>\n      <Skeleton height=\"45px\" />\n      <Skeleton width=\"330px\" height=\"55px\" />\n      <div className={s.playlistsTab}>\n        {Array.from({ length: 5 }).map((_, index) => (\n          <PlaylistCardSkeleton key={index} />\n        ))}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/UserTabsSkeleton/index.ts",
    "content": "export * from './UserTabsSkeleton'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/UserTabs/index.ts",
    "content": "export * from './UserTabs'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/UserPage/ui/index.ts",
    "content": "export * from './UserInfo'\nexport * from './UserTabs'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/auth/OAuthRedirect/OAuthCallback.module.css",
    "content": ".title {\n  text-align: center;\n  font-size: 250px;\n  margin: 0;\n}\n\n.subtitle {\n  text-align: center;\n  font-size: 50px;\n  margin: 0;\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/auth/OAuthRedirect/OAuthCallback.tsx",
    "content": "import { useEffect } from 'react'\n\nexport const OAuthCallback = () => {\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const code = url.searchParams.get('code') // или code/state, если flow другой\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, '*') // Лучше заменить \"*\" на точный origin\n    }\n\n    window.close()\n  }, [])\n\n  return <p>Welcome...</p>\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/ContentList/ContentList.module.css",
    "content": ".title {\n  margin-bottom: 20px;\n}\n\n.list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--list-gap, 8px);\n  padding-bottom: 8px;\n}\n\n.listRow {\n  display: flex;\n  flex-direction: column;\n  gap: 0;\n  padding: 40px 0;\n}\n\n.listRow li {\n  width: 100%;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/ContentList/ContentList.tsx",
    "content": "import clsx from 'clsx'\n\nimport { Typography } from '@/shared/components/Typography/Typography'\n\nimport s from './ContentList.module.css'\n\ntype ContentListProps<T> = {\n  title?: string\n  data?: T[]\n  renderItem: (item: T) => React.ReactNode\n  listClassName?: string\n  isLoading?: boolean\n  skeleton?: React.ReactNode\n  emptyMessage?: string\n  layout?: 'column' | 'row'\n}\n\nconst SKELETON_ITEM_COUNT = 10\n\nexport const ContentList = <T,>({\n  title,\n  data = [],\n  renderItem,\n  listClassName,\n  layout = 'column',\n  isLoading,\n  skeleton,\n  emptyMessage,\n}: ContentListProps<T>) => {\n  if (data.length === 0 && !isLoading && emptyMessage) {\n    return <Typography variant=\"body2\">{emptyMessage}</Typography>\n  }\n\n  return (\n    <section>\n      {title && (\n        <Typography variant=\"h2\" className={s.title}>\n          {title}\n        </Typography>\n      )}\n      <ul className={clsx(s.list, layout === 'row' && s.listRow, listClassName)}>\n        {isLoading && skeleton\n          ? Array.from({ length: SKELETON_ITEM_COUNT }).map((_, i) => <li key={i}>{skeleton}</li>)\n          : data.map((item, index) => <li key={index}>{renderItem(item)}</li>)}\n      </ul>\n    </section>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/ContentList/index.ts",
    "content": "export * from './ContentList'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/PageWithHeader/PageWithHeader.module.css",
    "content": ".wrapper {\n  min-height: calc(100vh - var(--header-height) - var(--player-height));\n  padding: 30px 40px;\n  background: linear-gradient(\n    180deg,\n    var(--page-gradient-color, #3333a3) 0,\n    var(--color-bg-secondary) 300px,\n    var(--color-bg-secondary) 100%\n  );\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/PageWithHeader/PageWithHeader.tsx",
    "content": "import clsx from 'clsx'\n\nimport s from './PageWithHeader.module.css'\n\ntype PageWithHeaderProps = {\n  children: React.ReactNode\n  className?: string\n}\n\nexport const PageWithHeader = ({ children, className }: PageWithHeaderProps) => {\n  return <div className={clsx(s.wrapper, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/PageWithoutHeader/PageWithoutHeader.module.css",
    "content": ".wrapper {\n  min-height: calc(100vh - var(--player-height));\n  margin-top: calc(0px - var(--header-height));\n  padding: var(--header-height) 40px 0;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/PageWithoutHeader/PageWithoutHeader.tsx",
    "content": "import clsx from 'clsx'\n\nimport s from './PageWithoutHeader.module.css'\n\ntype PageWithoutHeaderProps = {\n  children: React.ReactNode\n  className?: string\n  backgroundColor?: string\n}\n\nexport const PageWithoutHeader = ({\n  children,\n  className,\n  backgroundColor,\n}: PageWithoutHeaderProps) => {\n  const inlineStyles = backgroundColor\n    ? { background: `linear-gradient(180deg, ${backgroundColor} 0, #141414 300px, #141414 100%)` }\n    : {}\n\n  return (\n    <div className={clsx(s.wrapper, className)} style={inlineStyles}>\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/PageWrapper/PageWrapper.module.css",
    "content": ".wrapper {\n  min-height: calc(100vh - var(--header-height));\n  padding: 30px 40px;\n  background: linear-gradient(\n    180deg,\n    var(--page-gradient-color, #3333a3) 0,\n    var(--color-bg-secondary) 300px,\n    var(--color-bg-secondary) 100%\n  );\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/PageWrapper/PageWrapper.tsx",
    "content": "import clsx from 'clsx'\n\nimport s from './PageWrapper.module.css'\n\ntype PageWrapperProps = {\n  children: React.ReactNode\n  className?: string\n}\n\nexport const PageWrapper = ({ children, className }: PageWrapperProps) => {\n  return <div className={clsx(s.wrapper, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/PageWrapper/index.ts",
    "content": "export * from './PageWrapper'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/SearchTextField/SearchTextField.tsx",
    "content": "import { TextField, type TextFieldProps } from '@/shared/components'\nimport { SearchIcon } from '@/shared/icons'\n\nexport const SearchTextField = (props: Omit<TextFieldProps, 'icon' | 'inputSize'>) => {\n  return (\n    <TextField\n      {...props}\n      icon={<SearchIcon width={20} height={20} />}\n      inputSize=\"l\"\n      autoComplete=\"off\"\n    />\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/SearchTextField/index.ts",
    "content": "export * from './SearchTextField'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/SortSelect/SortSelect.module.css",
    "content": ".selectLabel {\n  display: flex;\n  flex-shrink: 0;\n  gap: 8px;\n  align-items: center;\n\n  width: 210px;\n}\n\n.select {\n  flex-shrink: 0;\n  width: 145px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/SortSelect/SortSelect.tsx",
    "content": "import { Select, type SelectProps } from '@/shared/components'\nimport { useTranslation } from 'react-i18next'\n\nimport s from './SortSelect.module.css'\n\nexport const SortSelect = (props: Omit<SelectProps, 'options'>) => {\n  const { t } = useTranslation()\n\n  return (\n    <label className={s.selectLabel}>\n      {t('sort.label')}\n      <Select\n        {...props}\n        options={[\n          {\n            value: 'newest',\n            label: t('sort.newest_first'),\n          },\n          {\n            value: 'oldest',\n            label: t('sort.oldest_first'),\n          },\n          {\n            value: 'mostLiked',\n            label: t('sort.most_liked'),\n          },\n          {\n            value: 'leastLiked',\n            label: t('sort.least_liked'),\n          },\n        ]}\n        className={s.select}\n      />\n    </label>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/SortSelect/index.ts",
    "content": "export * from './SortSelect'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/common/index.ts",
    "content": "export * from './ContentList'\nexport * from './PageWrapper'\nexport * from './PageWithHeader/PageWithHeader'\nexport * from './PageWithoutHeader/PageWithoutHeader'\nexport * from './SearchTextField'\nexport * from './SortSelect'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/pages/index.ts",
    "content": "export * from './MainPage'\nexport * from './PlaylistPage'\nexport * from './PlaylistsPage'\nexport * from './TrackLyricsPage'\nexport * from './TrackPage'\nexport * from './TracksPage'\nexport * from './UserPage'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/README.md",
    "content": "# Music Player - Business Logic\n\nThis folder contains the complete business logic for the music player, implemented with Zustand.\n\n## Setup\n\n### 1. Initialize Player\n\nImport and call the initialization function once in your app (e.g., in `main.tsx`):\n\n```typescript\n// src/main.tsx\nimport { initializePlayer } from '@/player'\n\ninitializePlayer()\n```\n\nYou can also clean up listeners when needed (e.g., in tests or hot reload):\n\n```typescript\nimport { cleanupPlayer } from '@/player'\n\ncleanupPlayer()\n```\n\n### 2. Basic Usage in Components\n\n#### Simple Track Item Component\n\n```tsx\nimport { useTrackPlayer } from '@/player'\n\nfunction TrackItem({ track }: { track: Track }) {\n  const { isPlaying, isPaused, isCurrentTrack, progress, togglePlayPause } = useTrackPlayer(track)\n\n  return (\n    <div className=\"track-item\">\n      <button onClick={togglePlayPause}>{isPlaying ? '⏸' : '▶'}</button>\n\n      {/* Only show progress bar for current track */}\n      {isCurrentTrack && (\n        <div className=\"progress-bar\">\n          <div style={{ width: `${progress}%` }} />\n        </div>\n      )}\n    </div>\n  )\n}\n```\n\n#### Player Controls Component\n\n```tsx\nimport { usePlayerControls, usePlaybackState, useCurrentTrack, useTrackNavigation } from '@/player'\n\nfunction PlayerControls() {\n  const { togglePlayPause } = usePlayerControls()\n  const { isPlaying } = usePlaybackState()\n  const { track } = useCurrentTrack()\n  const { next, previous, hasNext, hasPrevious } = useTrackNavigation()\n\n  return (\n    <div className=\"player-controls\">\n      <button onClick={previous} disabled={!hasPrevious}>\n        ⏮\n      </button>\n      <button onClick={togglePlayPause}>{isPlaying ? '⏸' : '▶'}</button>\n      <button onClick={next} disabled={!hasNext}>\n        ⏭\n      </button>\n\n      {track && (\n        <div className=\"now-playing\">\n          <span>{track.title}</span>\n        </div>\n      )}\n    </div>\n  )\n}\n```\n\n#### Progress Bar Component\n\n```tsx\nimport { usePlaybackProgress, usePlayerControls } from '@/player'\n\nfunction ProgressBar() {\n  const { currentTime, duration, progress, formattedTime } = usePlaybackProgress()\n  const { seek } = usePlayerControls()\n\n  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    const rect = e.currentTarget.getBoundingClientRect()\n    const x = e.clientX - rect.left\n    const percentage = x / rect.width\n    seek(percentage * duration)\n  }\n\n  return (\n    <div className=\"progress-container\">\n      <span>{formattedTime.current}</span>\n      <div className=\"progress-bar\" onClick={handleClick}>\n        <div style={{ width: `${progress}%` }} />\n      </div>\n      <span>{formattedTime.duration}</span>\n    </div>\n  )\n}\n```\n\n#### Volume Control Component\n\n```tsx\nimport { useVolumeControl, usePlayerControls } from '@/player'\n\nfunction VolumeControl() {\n  const { volume, isMuted, volumePercentage } = useVolumeControl()\n  const { setVolume, toggleMute } = usePlayerControls()\n\n  return (\n    <div className=\"volume-control\">\n      <button onClick={toggleMute}>{isMuted ? '🔇' : '🔊'}</button>\n      <input\n        type=\"range\"\n        min=\"0\"\n        max=\"1\"\n        step=\"0.01\"\n        value={isMuted ? 0 : volume}\n        onChange={(e) => setVolume(parseFloat(e.target.value))}\n      />\n    </div>\n  )\n}\n```\n\n#### Playback Modes Component\n\n```tsx\nimport { usePlaybackModes } from '@/player'\n\nfunction PlaybackModes() {\n  const { repeatMode, shuffleMode, cycleRepeatMode, toggleShuffle } = usePlaybackModes()\n\n  return (\n    <div className=\"playback-modes\">\n      <button onClick={toggleShuffle} className={shuffleMode ? 'active' : ''}>\n        🔀\n      </button>\n\n      <button onClick={cycleRepeatMode}>\n        {repeatMode === 'one' ? '🔂' : repeatMode === 'all' ? '🔁' : '↻'}\n      </button>\n    </div>\n  )\n}\n```\n\n#### Playlist Component\n\n```tsx\nimport { useQueueControls } from '@/player'\nimport type { Track } from '@/player'\n\nfunction PlaylistView({ playlistId, tracks }: { playlistId: string; tracks: Track[] }) {\n  const { loadPlaylist } = useQueueControls()\n\n  const handlePlayAll = () => {\n    loadPlaylist(playlistId, tracks, 0)\n  }\n\n  return (\n    <div>\n      <button onClick={handlePlayAll}>Play All</button>\n\n      <div className=\"track-list\">\n        {tracks.map((track) => (\n          <TrackItem key={track.id} track={track} />\n        ))}\n      </div>\n    </div>\n  )\n}\n```\n\n## Available Hooks\n\n### Core Hooks\n\n- **`usePlayer()`** - All-in-one hook with complete player functionality\n- **`usePlayerControls()`** - Play, pause, stop, seek, navigation, volume and mute controls\n- **`usePlaybackState()`** - Current playback state (playing, paused, loading, etc.)\n- **`useCurrentTrack()`** - Current playing track information\n- **`usePlaybackProgress()`** - Time, duration, progress percentage\n- **`usePlayingTrackProgress()`** - Current track progress (shorthand)\n\n### Track-Specific Hooks (Performance Optimized)\n\n- **`useTrackPlayer(track)`** - Complete player state and controls for specific track\n- **`useTrackPlaybackState(trackId)`** - Playback state for specific track\n- **`useTrackProgress(trackId)`** - Progress for specific track (only if current)\n- **`useIsCurrentTrack(trackId)`** - Check if track is currently playing\n- **`useTrackQueuePosition(trackId)`** - Track's position in queue\n\n### Feature-Specific Hooks\n\n- **`useVolumeControl()`** - Volume and mute state (read-only)\n- **`usePlayerQueue()`** - Queue state and manipulation\n- **`usePlaybackModes()`** - Repeat and shuffle modes\n- **`useTrackNavigation()`** - Next/previous track navigation\n- **`useSetRepeatMode()`** - Set specific repeat mode\n- **`useCycleRepeatMode()`** - Cycle through repeat modes\n- **`useToggleShuffle()`** - Toggle shuffle on/off\n- **`usePlayerKeyboardControls()`** - Enable keyboard shortcuts\n\n## Store Actions\n\n### Playback Control\n\n```typescript\nimport { usePlayerStore } from '@/player'\n\nconst { play, pause, resume, stop, togglePlayPause } = usePlayerStore.getState()\n\nplay(track, playlistId?, tracks?)\npause()\nresume()\nstop()\ntogglePlayPause()\n```\n\n### Navigation\n\n```typescript\nconst { nextTrack, previousTrack, playTrackAtIndex } = usePlayerStore.getState()\n\nnextTrack()\npreviousTrack()\nplayTrackAtIndex(index)\n```\n\n### Progress\n\n```typescript\nconst { seek } = usePlayerStore.getState()\n\nseek(timeInSeconds)\n```\n\n### Volume\n\n```typescript\nconst { setVolume, toggleMute } = usePlayerStore.getState()\n\nsetVolume(0.5) // 0-1\ntoggleMute()\n```\n\n### Modes\n\n```typescript\nconst { setRepeatMode, toggleShuffle } = usePlayerStore.getState()\n\nsetRepeatMode('off' | 'one' | 'all')\ntoggleShuffle()\n```\n\n### Queue\n\n```typescript\nconst { loadPlaylist, addToQueue, insertNext, removeFromQueue, clearQueue } = usePlayerStore.getState()\n\nloadPlaylist(playlistId, tracks, startIndex?)\naddToQueue([track1, track2])\ninsertNext(track)\nremoveFromQueue(index)\nclearQueue()\n```\n\n## Performance Considerations\n\n### Track List Optimization\n\nWhen rendering lists of tracks, use track-specific hooks to prevent unnecessary re-renders. These hooks are optimized with `useMemo` and atomic state selection.\n\n```tsx\n// ✅ Good - only re-renders when THIS track's state changes\nfunction TrackItem({ track }) {\n  const { isPlaying, isCurrentTrack } = useTrackPlayer(track)\n  // ...\n}\n\n// ❌ Bad - re-renders on any player state change (like currentTime updating)\nfunction TrackItem({ track }) {\n  const state = usePlayerStore()\n  const isPlaying = state.currentTrackId === track.id && state.playbackState === 'playing'\n  // ...\n}\n```\n\n### Component Memoization\n\nWrap track components in `React.memo` to ensure they only re-render when their props (like the `track` object) or the hooks they use trigger an update.\n\n```tsx\nexport default React.memo(TrackItem)\n```\n\n## File Structure\n\n```\nsrc/player/\n├── index.ts                  # Main exports\n├── README.md                 # This file\n├── task.md                   # Original task specification\n├── model/\n│   ├── player-store.ts       # Zustand store with all state and actions\n│   ├── player-track-hooks.ts # Track-specific hooks (performance critical)\n│   ├── player-hooks.ts       # Global React hooks\n│   ├── audio-manager.ts      # Singleton Audio wrapper\n│   └── utils/\n│       ├── index.ts          # Utils exports\n│       ├── shuffle.ts        # Shuffle algorithms\n│       ├── format-time.ts    # Time formatting\n│       ├── track-navigation.ts # Queue navigation logic\n│       └── convert-api-track-to-player-track.ts # API mappers\n└── types/\n    └── player.types.ts       # TypeScript types\n```\n\n## Persistence\n\nThe player automatically persists to localStorage:\n\n- Volume level\n- Repeat mode\n- Shuffle mode\n\nThese values are restored on page reload.\n\n## Error Handling\n\nErrors are automatically captured and stored in state:\n\n```tsx\nconst { error } = usePlaybackState()\n\nif (error) {\n  return <ErrorMessage message={error} />\n}\n```\n\n## Audio Manager\n\nThe `AudioManager` is a singleton that wraps the browser's `Audio` API:\n\n```typescript\nimport { audioManager } from '@/player'\n\naudioManager.loadTrack(track)\naudioManager.play()\naudioManager.pause()\naudioManager.seek(time)\naudioManager.setVolume(volume)\naudioManager.setMuted(muted)\n\n// Event listeners\naudioManager.on('timeupdate', (time) => {})\naudioManager.on('ended', () => {})\naudioManager.on('error', (error) => {})\naudioManager.on('loadedmetadata', ({ duration }) => {})\n```\n\n## Track Types\n\n```typescript\ninterface Track {\n  id: string\n  title: string\n  artist: string\n  album?: string\n  duration: number // in seconds\n  url: string\n  albumArt?: string\n  artistId?: string\n  albumId?: string\n}\n```\n\n## Queue Management\n\nThe player supports a full queue system:\n\n- **Original Queue**: Preserves original track order\n- **Shuffle Queue**: Randomized order when shuffle is enabled\n- **Queue Position**: Current index in the queue\n\nWhen shuffle is toggled, the queue is automatically reorganized while keeping the current track at the current position.\n\n## Keyboard Shortcuts\n\nEnable keyboard controls in your main App component:\n\n```typescript\nimport { usePlayerKeyboardControls } from '@/player'\n\nfunction App() {\n  usePlayerKeyboardControls(true)\n  return <YourApp />\n}\n```\n\n**Available shortcuts:**\n\n- `Space` - Play/Pause\n- `Arrow Right` - Seek forward 5s\n- `Arrow Left` - Seek backward 5s\n- `Arrow Up` - Volume up\n- `Arrow Down` - Volume down\n- `M` - Toggle mute\n- `N` - Next track\n- `P` - Previous track\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/SPECIFICATION.md",
    "content": "# Music Player Zustand Store - Technical Specification\n\n## 1. Overview\n\nThis specification defines a Zustand store that wraps the HTML5 Audio API to create a fully-featured music player with playlist support, multiple playback modes, and performance-optimized state management.\n\n## 2. Architecture\n\n### 2.1 Core Components\n\n```\nsrc/player/\n├── model/\n│   ├── player-store.ts      # Main Zustand store\n│   ├── player-track-hooks.ts # Track-specific hooks (performance critical)\n│   ├── player-hooks.ts     # Global React hooks\n│   ├── audio-manager.ts    # Singleton Audio wrapper\n│   └── utils/\n│       ├── shuffle.ts      # Fisher-Yates shuffle\n│       ├── format-time.ts  # Time formatting\n│       ├── track-navigation.ts # Queue navigation logic\n│       └── convert-api-track-to-player-track.ts # API mappers\n├── types/\n│   └── player.types.ts     # TypeScript interfaces\n└── index.ts                # Main exports\n```\n\n### 2.2 State Management Pattern\n\nThe store uses `zustand` with persistence middleware:\n\n```typescript\nimport { create } from 'zustand'\nimport { persist, createJSONStorage } from 'zustand/middleware'\n\nexport const usePlayerStore = create<PlayerStore>()(\n  persist(\n    (set, get) => ({\n      // State and actions\n    }),\n    {\n      name: 'musicfun-player',\n      storage: createJSONStorage(() => localStorage),\n      partialize: (state) => ({\n        // Persist only volume and modes\n        volume: state.volume,\n        repeatMode: state.repeatMode,\n        shuffleMode: state.shuffleMode,\n      }),\n    }\n  )\n)\n```\n\n## 3. State Structure\n\n```typescript\ninterface Track {\n  id: string\n  title: string\n  artist: string\n  duration: number // in seconds\n  url: string\n  albumArt?: string\n  artistId?: string\n  albumId?: string\n}\n\ninterface PlayerState {\n  // Current playback state\n  currentTrackId: string | null\n  currentPlaylistId: string | null\n  playbackState: 'idle' | 'playing' | 'paused' | 'loading' | 'error'\n\n  // Playback position\n  currentTime: number\n  duration: number\n  buffered: number // percentage 0-100\n\n  // Volume control\n  volume: number // 0-1\n  isMuted: boolean\n\n  // Playback modes\n  repeatMode: 'off' | 'one' | 'all'\n  shuffleMode: boolean\n\n  // Queue management\n  queue: string[] // ordered track IDs\n  originalQueue: string[] // original order before shuffle\n  queueIndex: number\n\n  // Track entities - normalized storage\n  tracks: Record<string, Track>\n\n  // Error handling\n  error: string | null\n\n  // Additional metadata\n  isLoadingTrack: boolean\n  hasNextTrack: boolean\n  hasPreviousTrack: boolean\n}\n```\n\n## 4. Core Features\n\n### 4.1 Playback Control\n\n#### Play\n\n- **Behavior**: Start playing the specified track or resume current track\n- **Rules**:\n  - If a different track is requested, stop current track and start new one from beginning\n  - If same track is requested while paused, resume from current position\n  - Only one track can play at a time\n  - Update playback state to 'loading' then 'playing'\n\n#### Pause\n\n- **Behavior**: Pause current track without resetting position\n- **Rules**:\n  - Can only pause if a track is currently playing\n  - Preserve currentTime for resume\n  - Update playback state to 'paused'\n\n### 4.2 Track Navigation\n\n#### Next Track\n\n- **Behavior**: Play the next track in queue\n- **Rules**:\n  - If repeatMode is 'one', replay current track from beginning\n  - If shuffleMode is on, use shuffled queue order\n  - If at end of queue:\n    - If repeatMode is 'all', go to first track\n    - If repeatMode is 'off', stop playback\n  - Start new track from beginning\n\n#### Previous Track\n\n- **Behavior**: Play the previous track or restart current track\n- **Rules**:\n  - If currentTime > 3 seconds, restart current track from beginning\n  - Otherwise, go to previous track in queue\n  - If at beginning of queue:\n    - If repeatMode is 'all', go to last track\n    - If repeatMode is 'off', restart current track\n\n### 4.3 Progress Tracking\n\n#### Seek\n\n- **Behavior**: Jump to specific position in current track\n- **Rules**:\n  - Validate position is within track duration\n  - Update currentTime\n  - Maintain current playback state (playing/paused)\n\n#### Time Update\n\n- **Behavior**: Sync state with Audio element time\n- **Rules**:\n  - Throttled updates (max 1 per 0.5s to avoid excessive re-renders)\n  - Update currentTime, buffered percentage\n  - Check for track end condition\n\n### 4.4 Volume Control\n\n#### Set Volume\n\n- **Behavior**: Adjust playback volume\n- **Rules**:\n  - Clamp value between 0 and 1\n  - Persist to localStorage\n  - If volume > 0, unmute automatically\n\n#### Toggle Mute\n\n- **Behavior**: Mute/unmute audio\n- **Rules**:\n  - Preserve volume level when muting\n  - Restore previous volume when unmuting\n\n### 4.5 Playback Modes\n\n#### Repeat Modes\n\n- **off**: Play queue once and stop\n- **one**: Repeat current track indefinitely\n- **all**: Loop entire queue continuously\n\n#### Shuffle Mode\n\n- **Behavior**: Randomize playback order\n- **Rules**:\n  - Store original queue order in originalQueue\n  - Generate shuffled queue using Fisher-Yates algorithm\n  - Maintain current track position when toggling\n  - When shuffle is disabled, restore original order\n\n### 4.6 Queue Management\n\n#### Load Playlist\n\n- **Behavior**: Load tracks from playlist into queue\n- **Rules**:\n  - Replace current queue\n  - Reset queue index to 0\n  - Store playlist ID for reference\n  - Apply shuffle if enabled\n  - Start playing first track\n\n#### Add to Queue\n\n- **Behavior**: Append track(s) to current queue\n- **Rules**:\n  - Add to end of queue\n  - Update originalQueue if shuffle is off\n  - Don't interrupt current playback\n\n#### Insert Next\n\n- **Behavior**: Add track to play after current track\n- **Rules**:\n  - Insert at queueIndex + 1\n  - Don't interrupt current playback\n\n#### Remove from Queue\n\n- **Behavior**: Remove track from queue\n- **Rules**:\n  - Adjust queueIndex if necessary\n  - If removing current track, skip to next\n\n## 5. Store Actions\n\n### Playback Control\n\n```typescript\nplay(track: Track, playlistId?: string, tracks?: Track[])\npause()\nresume()\nstop()\ntogglePlayPause()\n```\n\n### Navigation\n\n```typescript\nnextTrack()\npreviousTrack()\nplayTrackAtIndex(index: number)\nhandleTrackEnded()\n```\n\n### Progress\n\n```typescript\nseek(time: number)\nupdateTime(time: number)\nupdateBuffered(percentage: number)\nsetDuration(duration: number)\n```\n\n### Volume\n\n```typescript\nsetVolume(volume: number)\ntoggleMute()\n```\n\n### Modes\n\n```typescript\nsetRepeatMode(mode: 'off' | 'one' | 'all')\ntoggleShuffle()\n```\n\n### Queue\n\n```typescript\nloadPlaylist(playlistId: string, tracks: Track[], startIndex?: number)\naddToQueue(tracks: Track[])\ninsertNext(track: Track)\nremoveFromQueue(index: number)\nclearQueue()\n```\n\n### Error Handling\n\n```typescript\nsetError(error: string)\nclearError()\n```\n\n## 6. Performance Optimization\n\n### 6.1 Atomic Selection Pattern\n\nThe store uses atomic state selection to minimize re-renders. Hooks subscribe only to specific primitives, and complex objects are memoized using `useMemo`.\n\n```typescript\n// Hook example using atomic selection\nexport function usePlaybackState() {\n  const isPlaying = usePlayerStore((state) => state.playbackState === 'playing')\n  const playbackState = usePlayerStore((state) => state.playbackState)\n\n  return { isPlaying, playbackState }\n}\n```\n\n### 6.2 Track List Optimization\n\n**Problem**: With hundreds of tracks on page, we need to avoid re-rendering all track components when only one track's state changes or when `currentTime` updates.\n\n**Solution**: Use track-specific hooks that perform checks inside the selector.\n\n```typescript\n// In player-track-hooks.ts\nexport function useTrackPlaybackState(trackId: string): TrackPlaybackState {\n  return usePlayerStore((state) => ({\n    isCurrentTrack: state.currentTrackId === trackId,\n    isPlaying: state.currentTrackId === trackId && state.playbackState === 'playing',\n    // ...\n  }))\n}\n```\n\n### 6.3 Component Optimization\n\n- Wrap track components in `React.memo`\n- Use track-specific selectors to prevent unnecessary re-renders\n- Only subscribe to state slices that component needs\n\n### 6.4 Time Update Throttling\n\nThe AudioManager throttles timeupdate events to avoid excessive state updates:\n\n```typescript\nprivate bindEvents() {\n  this.audio.addEventListener('timeupdate', () => {\n    const currentTime = this.audio.currentTime\n    // Throttle to max 1 update per 0.5 seconds\n    if (Math.abs(currentTime - this.lastTimeUpdate) > 0.5) {\n      this.lastTimeUpdate = currentTime\n      this.emit('timeupdate', currentTime)\n    }\n  })\n}\n```\n\n## 7. Audio Integration\n\n### 7.1 Audio Manager\n\nThe AudioManager is a singleton that wraps the browser's Audio API:\n\n```typescript\nclass AudioManager {\n  private static instance: AudioManager\n  private audio: HTMLAudioElement\n  private listeners: Map<AudioEvent, Set<Function>> = new Map()\n\n  static get(): AudioManager {\n    if (!AudioManager.instance) {\n      AudioManager.instance = new AudioManager()\n    }\n    return AudioManager.instance\n  }\n\n  on<K>(event: K, callback: (data: any) => void) {\n    if (!this.listeners.has(event)) {\n      this.listeners.set(event, new Set())\n    }\n    this.listeners.get(event)!.add(callback)\n  }\n\n  off<K>(event: K, callback: (data: any) => void) {\n    const eventListeners = this.listeners.get(event)\n    if (eventListeners) {\n      eventListeners.delete(callback)\n    }\n  }\n\n  async loadTrack(track: Track): Promise<void> {\n    if (this.audio.src !== track.url) {\n      this.audio.src = track.url\n      if (this.audio.readyState >= 2) {\n        return Promise.resolve()\n      }\n      return new Promise((resolve, reject) => {\n        const onCanPlay = () => {\n          this.off('canplay', onCanPlay)\n          this.off('error', onError)\n          resolve()\n        }\n        const onError = () => {\n          this.off('canplay', onCanPlay)\n          this.off('error', onError)\n          reject(new Error('Failed to load track'))\n        }\n        this.on('canplay', onCanPlay)\n        this.on('error', onError)\n      })\n    }\n  }\n}\n\nexport const audioManager = AudioManager.get()\n```\n\n### 7.2 Audio Event Listeners\n\nThe store subscribes to `audioManager` events to update state:\n\n```typescript\naudioManager.on('timeupdate', (time) => {\n  usePlayerStore.setState({ currentTime: time })\n})\n\naudioManager.on('loadedmetadata', ({ duration }) => {\n  usePlayerStore.setState({ duration, isLoadingTrack: false })\n})\n\naudioManager.on('ended', () => {\n  usePlayerStore.getState().handleTrackEnded()\n})\n\naudioManager.on('error', (error) => {\n  usePlayerStore.setState({ playbackState: 'error', error })\n})\n\naudioManager.on('waiting', () => {\n  usePlayerStore.setState({ isLoadingTrack: true })\n})\n\naudioManager.on('canplay', () => {\n  usePlayerStore.setState({ isLoadingTrack: false })\n})\n```\n\n## 8. Component Integration Examples\n\n### 8.1 Track Item Component (in list of hundreds)\n\n```typescript\nimport { useTrackPlayer } from '@/player'\nimport type { Track } from '@/player'\n\ninterface TrackItemProps {\n  track: Track\n}\n\nconst TrackItem: React.FC<TrackItemProps> = ({ track }) => {\n  // This hook only causes re-render when THIS track's state changes\n  const { isPlaying, isPaused, isCurrentTrack, progress, togglePlayPause } =\n    useTrackPlayer(track)\n\n  return (\n    <div className=\"track-item\">\n      <button onClick={togglePlayPause}>{isPlaying ? '⏸' : '▶'}</button>\n      <span>{track.title}</span>\n      {isCurrentTrack && <ProgressBar progress={progress} />}\n    </div>\n  )\n}\n\nexport default React.memo(TrackItem)\n```\n\n### 8.2 Player Controls Component\n\n```typescript\nimport { usePlayerControls, usePlaybackState, useCurrentTrack, useTrackNavigation, useVolumeControl, usePlaybackModes } from '@/player'\n\nfunction PlayerControls() {\n  const { togglePlayPause, setVolume, toggleMute } = usePlayerControls()\n  const { isPlaying, isLoading, error } = usePlaybackState()\n  const { track } = useCurrentTrack()\n  const { next, previous, hasNext, hasPrevious } = useTrackNavigation()\n  const { volume, isMuted } = useVolumeControl()\n  const { repeatMode, shuffleMode, cycleRepeatMode, toggleShuffle } = usePlaybackModes()\n\n  return (\n    <div className=\"player-controls\">\n      <button onClick={previous} disabled={!hasPrevious}>\n        ⏮\n      </button>\n\n      <button onClick={togglePlayPause} disabled={isLoading}>\n        {isLoading ? '⏳' : isPlaying ? '⏸' : '▶'}\n      </button>\n\n      <button onClick={next} disabled={!hasNext}>\n        ⏭\n      </button>\n\n      <button onClick={toggleShuffle} className={shuffleMode ? 'active' : ''}>\n        🔀\n      </button>\n\n      <button onClick={cycleRepeatMode}>\n        {repeatMode === 'one' ? '🔂' : repeatMode === 'all' ? '🔁' : '↻'}\n      </button>\n\n      <button onClick={toggleMute}>{isMuted ? '🔇' : '🔊'}</button>\n      <input type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" value={volume} onChange={(e) => setVolume(parseFloat(e.target.value))} />\n\n      {track && (\n        <div className=\"now-playing\">\n          <img src={track.albumArt} alt=\"\" />\n          <div>\n            <div>{track.title}</div>\n            <div>{track.artist}</div>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n```\n\n### 8.3 Progress Bar Component\n\n```typescript\nimport { usePlaybackProgress, usePlayerControls } from '@/player'\n\nfunction ProgressBar() {\n  const { currentTime, duration, progress, formattedTime } = usePlaybackProgress()\n  const { seek } = usePlayerControls()\n\n  const handleSeek = (e: React.MouseEvent<HTMLDivElement>) => {\n    const rect = e.currentTarget.getBoundingClientRect()\n    const x = e.clientX - rect.left\n    const percentage = x / rect.width\n    seek(percentage * duration)\n  }\n\n  return (\n    <div className=\"progress-bar-container\">\n      <span>{formattedTime.current}</span>\n      <div className=\"progress-bar\" onClick={handleSeek}>\n        <div className=\"progress-fill\" style={{ width: `${progress}%` }} />\n      </div>\n      <span>{formattedTime.duration}</span>\n    </div>\n  )\n}\n```\n\n### 8.4 Playlist Component\n\n```typescript\nimport { useQueueControls } from '@/player'\nimport type { Track } from '@/player'\n\nfunction PlaylistView({ playlistId, tracks }: { playlistId: string; tracks: Track[] }) {\n  const { loadPlaylist } = useQueueControls()\n\n  const handlePlayAll = () => {\n    loadPlaylist(playlistId, tracks, 0)\n  }\n\n  return (\n    <div>\n      <h2>Playlist</h2>\n      <button onClick={handlePlayAll}>Play All</button>\n      <div className=\"track-list\">\n        {tracks.map((track) => (\n          <TrackItem key={track.id} track={track} />\n        ))}\n      </div>\n    </div>\n  )\n}\n```\n\n## 9. Persistence\n\nThe player automatically persists to localStorage:\n\n- Volume level\n- Repeat mode\n- Shuffle mode\n\n```typescript\nconst initialState = {\n  volume: loadPersistedVolume(),\n  repeatMode: loadPersistedRepeatMode(),\n  shuffleMode: loadPersistedShuffle(),\n}\n\n// Persistence helpers\nfunction loadPersistedVolume(): number {\n  try {\n    const volume = localStorage.getItem('player_volume')\n    return volume ? parseFloat(volume) : 1\n  } catch {\n    return 1\n  }\n}\n```\n\n## 10. Error Handling\n\nErrors are automatically captured and stored in state:\n\n```typescript\nconst { error } = usePlaybackState()\n\nif (error) {\n  return <ErrorMessage message={error} />\n}\n```\n\n## 11. Keyboard Shortcuts\n\nEnable keyboard controls in your main App component:\n\n```typescript\nimport { usePlayerKeyboardControls } from '@/player'\n\nfunction App() {\n  usePlayerKeyboardControls(true)\n  return <YourApp />\n}\n```\n\n**Available shortcuts:**\n\n- `Space` - Play/Pause\n- `Arrow Right` - Seek forward 5s\n- `Arrow Left` - Seek backward 5s\n- `Arrow Up` - Volume up\n- `Arrow Down` - Volume down\n- `M` - Toggle mute\n- `N` - Next track\n- `P` - Previous track\n\n## 12. Performance Metrics\n\n- Zero unnecessary re-renders of non-playing tracks\n- < 100ms response time for play/pause actions\n- < 50ms for progress bar updates\n- Support 1000+ tracks in UI without performance degradation\n- < 1s time to start playing track after selection\n- Smooth 60fps animations for progress bars\n\n## 13. API Contract\n\n### Track Type\n\n```typescript\ninterface Track {\n  id: string // Required - unique track identifier\n  title: string // Required - track title\n  artist: string // Required - artist name\n  url: string // Required - audio file URL (THIS IS CRITICAL!)\n  duration: number // Required - duration in seconds\n\n  // Optional fields\n  album?: string // Album name\n  albumArt?: string // Cover image URL\n  artistId?: string // Artist ID for navigation\n  albumId?: string // Album ID for navigation\n}\n```\n\n### Playlist Type\n\n```typescript\ninterface Playlist {\n  id: string\n  name: string\n  description?: string\n  trackIds: string[]\n  createdAt: string\n  updatedAt: string\n  coverImage?: string\n}\n```\n\n## 14. Future Enhancements\n\n- Media Session API integration\n- Gapless playback with preloading\n- Crossfade between tracks\n- Audio effects (equalizer, bass boost)\n- Playback speed control\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/index.ts",
    "content": "// Main store and initialization\nexport { usePlayerStore, initializePlayer, setupAudioListeners } from './model/player-store'\n\n// Hooks\nexport * from './model/player-hooks'\nexport * from './model/player-track-hooks'\n\n// Audio Manager\nexport { audioManager } from './model/audio-manager'\n\n// Types\nexport * from './types/player.types'\n\n// Utils\nexport * from './utils'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/model/audio-manager.ts",
    "content": "import type { Track } from '../types/player.types'\n\ntype AudioEventMap = {\n  timeupdate: number\n  loadedmetadata: { duration: number }\n  ended: void\n  error: string\n  playing: void\n  paused: void\n  waiting: void\n  canplay: void\n}\n\ntype EventCallback<K extends keyof AudioEventMap> = (data: AudioEventMap[K]) => void\n\nclass AudioManager {\n  private static instance: AudioManager\n  private audio: HTMLAudioElement\n  private listeners: Map<keyof AudioEventMap, Set<EventCallback<any>>> = new Map()\n  private lastTimeUpdate = 0\n\n  private constructor() {\n    this.audio = new Audio()\n    this.audio.preload = 'metadata'\n    this.audio.crossOrigin = 'anonymous'\n\n    this.bindEvents()\n  }\n\n  static get(): AudioManager {\n    if (!AudioManager.instance) {\n      AudioManager.instance = new AudioManager()\n    }\n    return AudioManager.instance\n  }\n\n  private bindEvents() {\n    this.audio.addEventListener('timeupdate', () => {\n      const currentTime = this.audio.currentTime\n      // Throttle timeupdate events to avoid excessive state updates\n      if (Math.abs(currentTime - this.lastTimeUpdate) > 0.5) {\n        this.lastTimeUpdate = currentTime\n        this.emit('timeupdate', currentTime)\n      }\n    })\n\n    this.audio.addEventListener('loadedmetadata', () => {\n      this.emit('loadedmetadata', { duration: this.audio.duration })\n    })\n\n    this.audio.addEventListener('ended', () => {\n      this.emit('ended', undefined)\n    })\n\n    this.audio.addEventListener('error', () => {\n      const errorMessage = this.audio.error?.message || 'Unknown error'\n      this.emit('error', errorMessage)\n    })\n\n    this.audio.addEventListener('play', () => {\n      this.emit('playing', undefined)\n    })\n\n    this.audio.addEventListener('pause', () => {\n      this.emit('paused', undefined)\n    })\n\n    this.audio.addEventListener('waiting', () => {\n      this.emit('waiting', undefined)\n    })\n\n    this.audio.addEventListener('canplay', () => {\n      this.emit('canplay', undefined)\n    })\n  }\n\n  on<K extends keyof AudioEventMap>(event: K, callback: EventCallback<K>) {\n    if (!this.listeners.has(event)) {\n      this.listeners.set(event, new Set())\n    }\n    this.listeners.get(event)!.add(callback)\n  }\n\n  off<K extends keyof AudioEventMap>(event: K, callback: EventCallback<K>) {\n    const eventListeners = this.listeners.get(event)\n    if (eventListeners) {\n      eventListeners.delete(callback)\n    }\n  }\n\n  private emit<K extends keyof AudioEventMap>(event: K, data: AudioEventMap[K]) {\n    const eventListeners = this.listeners.get(event)\n    if (eventListeners) {\n      eventListeners.forEach((callback) => callback(data))\n    }\n  }\n\n  async loadTrack(track: Track): Promise<void> {\n    if (this.audio.src !== track.url) {\n      this.audio.src = track.url\n\n      // Return immediately if already loaded\n      if (this.audio.readyState >= 2) {\n        return Promise.resolve()\n      }\n\n      return new Promise<void>((resolve, reject) => {\n        let timeoutId: ReturnType<typeof setTimeout> | null = null\n\n        const onCanPlay = () => {\n          if (timeoutId) clearTimeout(timeoutId)\n          this.off('canplay', onCanPlay as EventCallback<'canplay'>)\n          this.off('error', onError)\n          resolve()\n        }\n\n        const onError = (_error: string) => {\n          if (timeoutId) clearTimeout(timeoutId)\n          this.off('canplay', onCanPlay as EventCallback<'canplay'>)\n          this.off('error', onError)\n          reject(new Error(`Failed to load track: ${track.url}`))\n        }\n\n        this.on('canplay', onCanPlay as EventCallback<'canplay'>)\n        this.on('error', onError as EventCallback<'error'>)\n\n        // Set a timeout to avoid hanging\n        timeoutId = setTimeout(() => {\n          this.off('canplay', onCanPlay as EventCallback<'canplay'>)\n          this.off('error', onError)\n          reject(new Error('Track load timeout'))\n        }, 30000)\n      })\n    }\n  }\n\n  play(): Promise<void> {\n    return this.audio.play()\n  }\n\n  pause() {\n    this.audio.pause()\n  }\n\n  stop() {\n    this.audio.pause()\n    this.audio.currentTime = 0\n  }\n\n  seek(time: number) {\n    if (time >= 0 && time <= this.audio.duration) {\n      this.audio.currentTime = time\n    }\n  }\n\n  setVolume(volume: number) {\n    this.audio.volume = Math.max(0, Math.min(1, volume))\n  }\n\n  setMuted(muted: boolean) {\n    this.audio.muted = muted\n  }\n\n  getCurrentTime(): number {\n    return this.audio.currentTime\n  }\n\n  getDuration(): number {\n    return this.audio.duration\n  }\n\n  getPaused(): boolean {\n    return this.audio.paused\n  }\n\n  getSrc(): string {\n    return this.audio.src\n  }\n\n  destroy() {\n    this.listeners.clear()\n    this.audio.pause()\n    this.audio.src = ''\n    this.audio.load()\n  }\n}\n\nexport const audioManager = AudioManager.get()\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/model/player-hooks.ts",
    "content": "import { useCallback, useMemo, useEffect } from 'react'\nimport type { RepeatMode, Track } from '../types/player.types'\nimport {\n  useTrackPlaybackState,\n  useTrackProgress,\n  useTrackQueuePosition,\n} from './player-track-hooks'\nimport { formatTime } from '../utils'\nimport { usePlayerStore } from './player-store'\nimport { getNextTrackId, getPreviousTrackId, getQueuePosition } from '../utils/track-navigation'\n\n// ========================================\n// Hooks that use reactive state (trigger re-renders)\n// ========================================\n\nexport function usePlayingTrackProgress() {\n  const currentTime = usePlayerStore((state) => state.currentTime)\n  const duration = usePlayerStore((state) => state.duration)\n\n  const playingTrackProgress = useMemo(() => {\n    return duration > 0 ? (currentTime / duration) * 100 : 0\n  }, [currentTime, duration])\n\n  return useMemo(\n    () => ({\n      playingTrackProgress,\n      currentTime,\n      duration,\n    }),\n    [playingTrackProgress, currentTime, duration]\n  )\n}\n\nexport function usePlaybackState() {\n  const isPlaying = usePlayerStore((state) => state.playbackState === 'playing')\n  const isPaused = usePlayerStore((state) => state.playbackState === 'paused')\n  const isLoading = usePlayerStore(\n    (state) => state.playbackState === 'loading' || state.isLoadingTrack\n  )\n  const playbackState = usePlayerStore((state) => state.playbackState)\n  const error = usePlayerStore((state) => state.error)\n\n  return {\n    isPlaying,\n    isPaused,\n    isLoading,\n    playbackState,\n    error,\n  }\n}\n\nexport function useCurrentTrack() {\n  const trackId = usePlayerStore((state) => state.currentTrackId)\n  const track = usePlayerStore((state) =>\n    state.currentTrackId ? state.tracks[state.currentTrackId] || null : null\n  )\n  const isPlaying = usePlayerStore((state) => state.playbackState === 'playing')\n  const isPaused = usePlayerStore((state) => state.playbackState === 'paused')\n\n  return useMemo(\n    () => ({\n      trackId,\n      track,\n      isPlaying,\n      isPaused,\n    }),\n    [trackId, track, isPlaying, isPaused]\n  )\n}\n\nexport function usePlaybackProgress() {\n  const currentTime = usePlayerStore((state) => state.currentTime)\n  const duration = usePlayerStore((state) => state.duration)\n\n  const progress = useMemo(() => {\n    if (!duration || duration === 0) return 0\n    return (currentTime / duration) * 100\n  }, [currentTime, duration])\n\n  const formattedTime = useMemo(\n    () => ({\n      current: formatTime(currentTime),\n      duration: formatTime(duration),\n    }),\n    [currentTime, duration]\n  )\n\n  return {\n    currentTime,\n    duration,\n    progress,\n    formattedTime,\n  }\n}\n\nexport function useVolumeControl() {\n  const volume = usePlayerStore((state) => state.volume)\n  const isMuted = usePlayerStore((state) => state.isMuted)\n\n  const effectiveVolume = useMemo(() => {\n    return isMuted ? 0 : volume\n  }, [volume, isMuted])\n\n  const volumePercentage = useMemo(() => {\n    return Math.round(volume * 100)\n  }, [volume])\n\n  return useMemo(\n    () => ({\n      volume,\n      isMuted,\n      effectiveVolume,\n      volumePercentage,\n    }),\n    [volume, isMuted, effectiveVolume, volumePercentage]\n  )\n}\n\nexport function useQueue() {\n  const queue = usePlayerStore((state) => state.queue)\n  const queueIndex = usePlayerStore((state) => state.queueIndex)\n  const repeatMode = usePlayerStore((state) => state.repeatMode)\n  const queueTracks = usePlayerStore((state) => {\n    if (!state.queue.length) return []\n    return state.queue\n      .map((trackId: string) => state.tracks[trackId])\n      .filter((track): track is Track => track !== undefined)\n  })\n  const hasNext = usePlayerStore((state) => state.hasNextTrack)\n  const hasPrevious = usePlayerStore((state) => state.hasPreviousTrack)\n\n  const queuePosition = useMemo(\n    () => getQueuePosition(queueIndex, queue.length),\n    [queueIndex, queue.length]\n  )\n\n  const nextTrackId = useMemo(\n    () => getNextTrackId(queue, queueIndex, repeatMode),\n    [queue, queueIndex, repeatMode]\n  )\n\n  const previousTrackId = useMemo(\n    () => getPreviousTrackId(queue, queueIndex, repeatMode),\n    [queue, queueIndex, repeatMode]\n  )\n\n  return {\n    queue,\n    queueIndex,\n    queueTracks,\n    queuePosition,\n    hasNext,\n    hasPrevious,\n    nextTrackId,\n    previousTrackId,\n  }\n}\n\nexport function usePlaybackModes() {\n  const repeatMode = usePlayerStore((state) => state.repeatMode)\n  const shuffleMode = usePlayerStore((state) => state.shuffleMode)\n\n  const modeDescription = useMemo(() => {\n    const parts: string[] = []\n    if (shuffleMode) parts.push('Shuffle')\n    switch (repeatMode) {\n      case 'one':\n        parts.push('Repeat One')\n        break\n      case 'all':\n        parts.push('Repeat All')\n        break\n      case 'off':\n        parts.push('No Repeat')\n        break\n    }\n    return parts.join(', ')\n  }, [shuffleMode, repeatMode])\n\n  return {\n    repeatMode,\n    shuffleMode,\n    modeDescription,\n  }\n}\n\n// ========================================\n// Track-Specific Hooks (Performance Optimized)\n// ========================================\n\n/**\n * Hook for track-specific playback state\n * Only causes rerender when THIS track's state changes\n */\nexport function useTrackPlayer(track: Track) {\n  const { isPlaying, isPaused, isCurrentTrack } = useTrackPlaybackState(track.id)\n  const { progress, currentTime } = useTrackProgress(track.id)\n  const queuePosition = useTrackQueuePosition(track.id)\n\n  const play = useCallback(() => {\n    usePlayerStore.getState().play(track)\n  }, [track])\n\n  const pauseTrack = useCallback(() => {\n    usePlayerStore.getState().pause()\n  }, [])\n\n  const resumeTrack = useCallback(() => {\n    usePlayerStore.getState().resume()\n  }, [])\n\n  const togglePlayPauseTrack = useCallback(() => {\n    const store = usePlayerStore.getState()\n    const state = store\n    if (state.currentTrackId === track.id && state.playbackState === 'playing') {\n      store.pause()\n    } else if (state.currentTrackId === track.id && state.playbackState === 'paused') {\n      store.resume()\n    } else {\n      store.play(track)\n    }\n  }, [track])\n\n  return {\n    // State\n    isPlaying,\n    isPaused,\n    isCurrentTrack,\n    progress,\n    currentTime,\n    queuePosition,\n    // Actions\n    play,\n    pause: pauseTrack,\n    resume: resumeTrack,\n    togglePlayPause: togglePlayPauseTrack,\n  }\n}\n\n// ========================================\n// Combined Hooks\n// ========================================\n\nexport function usePlayerControls() {\n  return useMemo(\n    () => ({\n      play: (track: Track, playlistId?: string, tracks?: Track[]) =>\n        usePlayerStore.getState().play(track, playlistId, tracks),\n      pause: () => usePlayerStore.getState().pause(),\n      resume: () => usePlayerStore.getState().resume(),\n      stop: () => usePlayerStore.getState().stop(),\n      togglePlayPause: () => usePlayerStore.getState().togglePlayPause(),\n      next: () => usePlayerStore.getState().nextTrack(),\n      previous: () => usePlayerStore.getState().previousTrack(),\n      playAtIndex: (index: number) => usePlayerStore.getState().playTrackAtIndex(index),\n      seek: (time: number) => usePlayerStore.getState().seek(time),\n      setVolume: (volume: number) => usePlayerStore.getState().setVolume(volume),\n      toggleMute: () => usePlayerStore.getState().toggleMute(),\n    }),\n    []\n  )\n}\n\nexport function useQueueControls() {\n  return useMemo(\n    () => ({\n      loadPlaylist: (playlistId: string, tracks: Track[], startIndex?: number) =>\n        usePlayerStore.getState().loadPlaylist(playlistId, tracks, startIndex),\n      addToQueue: (tracks: Track[]) => usePlayerStore.getState().addToQueue(tracks),\n      insertNext: (track: Track) => usePlayerStore.getState().insertNext(track),\n      removeFromQueue: (index: number) => usePlayerStore.getState().removeFromQueue(index),\n      clearQueue: () => usePlayerStore.getState().clearQueue(),\n    }),\n    []\n  )\n}\n\nexport function usePlayerQueue() {\n  const queueState = useQueue()\n  const queueControls = useQueueControls()\n\n  return {\n    ...queueState,\n    ...queueControls,\n  }\n}\n\nexport function usePlayer() {\n  const controls = usePlayerControls()\n  const playbackState = usePlaybackState()\n  const currentTrack = useCurrentTrack()\n  const progress = usePlaybackProgress()\n  const volume = useVolumeControl()\n  const queue = usePlayerQueue()\n  const modes = usePlaybackModes()\n\n  return {\n    controls,\n    playbackState,\n    currentTrack,\n    progress,\n    volume,\n    queue,\n    modes,\n  }\n}\n\nexport function useTrackNavigation() {\n  const hasNext = usePlayerStore((state) => state.hasNextTrack)\n  const hasPrevious = usePlayerStore((state) => state.hasPreviousTrack)\n  const queue = usePlayerStore((state) => state.queue)\n  const queueIndex = usePlayerStore((state) => state.queueIndex)\n  const repeatMode = usePlayerStore((state) => state.repeatMode)\n\n  const nextTrackId = useMemo(\n    () => getNextTrackId(queue, queueIndex, repeatMode),\n    [queue, queueIndex, repeatMode]\n  )\n\n  const previousTrackId = useMemo(\n    () => getPreviousTrackId(queue, queueIndex, repeatMode),\n    [queue, queueIndex, repeatMode]\n  )\n\n  const goNext = useCallback(() => {\n    usePlayerStore.getState().nextTrack()\n  }, [])\n\n  const goPrevious = useCallback(() => {\n    usePlayerStore.getState().previousTrack()\n  }, [])\n\n  return useMemo(\n    () => ({\n      hasNext,\n      hasPrevious,\n      nextTrackId,\n      previousTrackId,\n      next: goNext,\n      previous: goPrevious,\n    }),\n    [hasNext, hasPrevious, nextTrackId, previousTrackId, goNext, goPrevious]\n  )\n}\n\n// ========================================\n// Mode Control Hooks\n// ========================================\n\nexport function useSetRepeatMode() {\n  return useCallback((mode: RepeatMode) => {\n    usePlayerStore.getState().setRepeatMode(mode)\n  }, [])\n}\n\nexport function useCycleRepeatMode() {\n  const repeatMode = usePlayerStore((state) => state.repeatMode)\n\n  return useCallback(() => {\n    const nextMode: RepeatMode = repeatMode === 'off' ? 'all' : repeatMode === 'all' ? 'one' : 'off'\n    usePlayerStore.getState().setRepeatMode(nextMode)\n  }, [repeatMode])\n}\n\nexport function useToggleShuffle() {\n  return useCallback(() => {\n    usePlayerStore.getState().toggleShuffle()\n  }, [])\n}\n\n// ========================================\n// Keyboard Controls Hook\n// ========================================\n\nexport function usePlayerKeyboardControls(enabled = true) {\n  const currentTime = usePlayerStore((state) => state.currentTime)\n  const duration = usePlayerStore((state) => state.duration)\n  const volume = usePlayerStore((state) => state.volume)\n\n  const handleKeyPress = useCallback(\n    (e: KeyboardEvent) => {\n      if (!enabled) return\n\n      // Don't trigger if user is typing in an input\n      const target = e.target as HTMLElement\n      if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {\n        return\n      }\n\n      const store = usePlayerStore.getState()\n      const state = store\n\n      switch (e.key.toLowerCase()) {\n        case ' ':\n          e.preventDefault()\n          store.togglePlayPause()\n          break\n\n        case 'arrowright':\n          e.preventDefault()\n          store.seek(Math.min(state.currentTime + 5, state.duration))\n          break\n\n        case 'arrowleft':\n          e.preventDefault()\n          store.seek(Math.max(state.currentTime - 5, 0))\n          break\n\n        case 'arrowup':\n          e.preventDefault()\n          store.setVolume(Math.min(state.volume + 0.1, 1))\n          break\n\n        case 'arrowdown':\n          e.preventDefault()\n          store.setVolume(Math.max(state.volume - 0.1, 0))\n          break\n\n        case 'm':\n          e.preventDefault()\n          store.toggleMute()\n          break\n\n        case 'n':\n          e.preventDefault()\n          store.nextTrack()\n          break\n\n        case 'p':\n          e.preventDefault()\n          store.previousTrack()\n          break\n      }\n    },\n    [enabled, currentTime, duration, volume]\n  )\n\n  // Set up keyboard event listener\n  useEffect(() => {\n    if (typeof window === 'undefined') return\n\n    if (enabled) {\n      window.addEventListener('keydown', handleKeyPress)\n      return () => window.removeEventListener('keydown', handleKeyPress)\n    }\n  }, [enabled, handleKeyPress])\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/model/player-store.ts",
    "content": "import { create } from 'zustand'\nimport { createJSONStorage, persist } from 'zustand/middleware'\nimport type { PlayerState, RepeatMode, Track } from '../types/player.types'\nimport { audioManager } from './audio-manager'\nimport { shuffle, shuffleWithCurrentItem } from '../utils'\n\n// ========================================\n// Persistence helpers\n// ========================================\n\nconst loadPersistedVolume = (): number => {\n  try {\n    const volume = localStorage.getItem('player_volume')\n    return volume ? parseFloat(volume) : 1\n  } catch {\n    return 1\n  }\n}\n\nconst loadPersistedRepeatMode = (): RepeatMode => {\n  try {\n    const mode = localStorage.getItem('player_repeat_mode')\n    return (mode as RepeatMode) || 'off'\n  } catch {\n    return 'off'\n  }\n}\n\nconst loadPersistedShuffle = (): boolean => {\n  try {\n    const shuffle = localStorage.getItem('player_shuffle')\n    return shuffle === 'true'\n  } catch {\n    return false\n  }\n}\n\n// ========================================\n// Initial State\n// ========================================\n\nconst initialState = {\n  // Current playback state\n  currentTrackId: null as string | null,\n  currentPlaylistId: null as string | null,\n  playbackState: 'idle' as PlayerState['playbackState'],\n\n  // Playback position\n  currentTime: 0,\n  duration: 0,\n  buffered: 0,\n\n  // Volume control\n  volume: loadPersistedVolume(),\n  isMuted: false,\n\n  // Playback modes\n  repeatMode: loadPersistedRepeatMode(),\n  shuffleMode: loadPersistedShuffle(),\n\n  // Queue management\n  queue: [] as string[],\n  originalQueue: [] as string[],\n  queueIndex: -1,\n\n  // Track entities - normalized storage\n  tracks: {} as Record<string, Track>,\n\n  // Error handling\n  error: null as string | null,\n\n  // Additional metadata\n  isLoadingTrack: false,\n  hasNextTrack: false,\n  hasPreviousTrack: false,\n}\n\n// ========================================\n// Zustand Store\n// ========================================\n\ntype PlayerStore = typeof initialState & {\n  // Playback actions\n  play: (track: Track, playlistId?: string, tracks?: Track[]) => void\n  pause: () => void\n  resume: () => void\n  stop: () => void\n  togglePlayPause: () => void\n\n  // Navigation actions\n  nextTrack: () => void\n  previousTrack: () => void\n  playTrackAtIndex: (index: number) => void\n  handleTrackEnded: () => void\n\n  // Progress actions\n  seek: (time: number) => void\n  updateTime: (time: number) => void\n  updateBuffered: (buffered: number) => void\n  setDuration: (duration: number) => void\n\n  // Volume actions\n  setVolume: (volume: number) => void\n  toggleMute: () => void\n\n  // Mode actions\n  setRepeatMode: (mode: RepeatMode) => void\n  toggleShuffle: () => void\n\n  // Queue actions\n  loadPlaylist: (playlistId: string, tracks: Track[], startIndex?: number) => void\n  addToQueue: (tracks: Track[]) => void\n  insertNext: (track: Track) => void\n  removeFromQueue: (index: number) => void\n  clearQueue: () => void\n\n  // Error actions\n  setError: (error: string) => void\n  clearError: () => void\n\n  // Internal helpers\n  updateQueueMetadata: () => void\n}\n\nexport const usePlayerStore = create<PlayerStore>()(\n  persist(\n    (set, get) => ({\n      ...initialState,\n\n      // ========================================\n      // Playback Control\n      // ========================================\n\n      play(track, playlistId?, tracks?) {\n        const state = get()\n\n        // Store track in entities\n        const newTracks = { ...state.tracks, [track.id]: track }\n        set({ tracks: newTracks })\n\n        // If this is a new track\n        if (state.currentTrackId !== track.id) {\n          set({\n            currentTrackId: track.id,\n            currentPlaylistId: playlistId || null,\n            currentTime: 0,\n            duration: 0,\n            buffered: 0,\n            playbackState: 'loading',\n            error: null,\n          })\n\n          // If tracks array is provided, set up queue with all tracks\n          if (tracks && tracks.length > 0) {\n            tracks.forEach((t) => {\n              newTracks[t.id] = t\n            })\n            set({ tracks: newTracks })\n\n            const trackIds = tracks.map((t) => t.id)\n            const newQueue = state.shuffleMode ? shuffle(trackIds) : trackIds\n            const newQueueIndex = newQueue.indexOf(track.id)\n\n            set({\n              queue: newQueue,\n              originalQueue: trackIds,\n              queueIndex: newQueueIndex,\n            })\n          } else if (state.queue.length === 0) {\n            // If no queue exists, create one with just this track\n            set({\n              queue: [track.id],\n              originalQueue: [track.id],\n              queueIndex: 0,\n            })\n          } else {\n            // Find track in existing queue\n            const index = state.queue.indexOf(track.id)\n            if (index !== -1) {\n              set({ queueIndex: index })\n            } else {\n              // Track not in queue, add it\n              const newQueue = [...state.queue, track.id]\n              const newOriginalQueue = [...state.originalQueue, track.id]\n              set({\n                queue: newQueue,\n                originalQueue: newOriginalQueue,\n                queueIndex: newQueue.length - 1,\n              })\n            }\n          }\n\n          // Load and play the track\n          audioManager\n            .loadTrack(track)\n            .then(() => {\n              audioManager.play()\n              set({ playbackState: 'playing' })\n            })\n            .catch((error) => {\n              set({\n                playbackState: 'error',\n                error: error.message,\n              })\n            })\n        } else if (state.playbackState === 'paused') {\n          // Same track, just resume\n          audioManager.play()\n          set({ playbackState: 'playing' })\n        } else if (state.playbackState === 'idle' || state.playbackState === 'error') {\n          // Same track after ended/stopped/error - reload source and play again\n          set({\n            currentTime: 0,\n            duration: 0,\n            buffered: 0,\n            playbackState: 'loading',\n            error: null,\n          })\n\n          audioManager\n            .loadTrack(track)\n            .then(() => {\n              audioManager.play()\n              set({ playbackState: 'playing' })\n            })\n            .catch((error) => {\n              set({\n                playbackState: 'error',\n                error: error.message,\n              })\n            })\n        }\n      },\n\n      pause() {\n        const state = get()\n        if (state.playbackState === 'playing') {\n          audioManager.pause()\n          set({ playbackState: 'paused' })\n        }\n      },\n\n      resume() {\n        const state = get()\n        if (state.playbackState === 'paused') {\n          audioManager.play()\n          set({ playbackState: 'playing' })\n        }\n      },\n\n      stop() {\n        audioManager.stop()\n        set({\n          playbackState: 'idle',\n          currentTime: 0,\n        })\n      },\n\n      togglePlayPause() {\n        const state = get()\n        if (state.playbackState === 'playing') {\n          audioManager.pause()\n          set({ playbackState: 'paused' })\n        } else if (state.playbackState === 'paused' || state.playbackState === 'idle') {\n          if (state.currentTrackId && state.tracks[state.currentTrackId]) {\n            audioManager.play()\n            set({ playbackState: 'playing' })\n          }\n        }\n      },\n\n      // ========================================\n      // Navigation\n      // ========================================\n\n      nextTrack() {\n        const state = get()\n        if (state.queue.length === 0) return\n\n        const isAtEnd = state.queueIndex >= state.queue.length - 1\n\n        if (isAtEnd) {\n          if (state.repeatMode === 'all') {\n            // Loop to beginning\n            const newIndex = 0\n            const newTrackId = state.queue[newIndex]\n            set({\n              queueIndex: newIndex,\n              currentTrackId: newTrackId,\n              currentTime: 0,\n              playbackState: 'loading',\n            })\n\n            // Play next track\n            const track = state.tracks[newTrackId]\n            if (track) {\n              audioManager\n                .loadTrack(track)\n                .then(() => {\n                  audioManager.play()\n                  set({ playbackState: 'playing' })\n                })\n                .catch((error) => {\n                  set({\n                    playbackState: 'error',\n                    error: error.message,\n                  })\n                })\n            }\n          } else {\n            // Stop playback\n            audioManager.stop()\n            set({\n              playbackState: 'idle',\n              currentTime: 0,\n            })\n          }\n        } else {\n          // Go to next track\n          const newIndex = state.queueIndex + 1\n          const newTrackId = state.queue[newIndex]\n          set({\n            queueIndex: newIndex,\n            currentTrackId: newTrackId,\n            currentTime: 0,\n            playbackState: 'loading',\n          })\n\n          const track = state.tracks[newTrackId]\n          if (track) {\n            audioManager\n              .loadTrack(track)\n              .then(() => {\n                audioManager.play()\n                set({ playbackState: 'playing' })\n              })\n              .catch((error) => {\n                set({\n                  playbackState: 'error',\n                  error: error.message,\n                })\n              })\n          }\n        }\n        get().updateQueueMetadata()\n      },\n\n      previousTrack() {\n        const state = get()\n        if (state.queue.length === 0) return\n\n        // If more than 3 seconds into track, restart current track\n        if (state.currentTime > 3) {\n          audioManager.seek(0)\n          set({ currentTime: 0 })\n          return\n        }\n\n        const isAtBeginning = state.queueIndex <= 0\n\n        if (isAtBeginning) {\n          if (state.repeatMode === 'all') {\n            // Loop to end\n            const newIndex = state.queue.length - 1\n            const newTrackId = state.queue[newIndex]\n            set({\n              queueIndex: newIndex,\n              currentTrackId: newTrackId,\n              currentTime: 0,\n              playbackState: 'loading',\n            })\n\n            const track = state.tracks[newTrackId]\n            if (track) {\n              audioManager\n                .loadTrack(track)\n                .then(() => {\n                  audioManager.play()\n                  set({ playbackState: 'playing' })\n                })\n                .catch((error) => {\n                  set({\n                    playbackState: 'error',\n                    error: error.message,\n                  })\n                })\n            }\n          } else {\n            // Restart current track\n            audioManager.seek(0)\n            set({ currentTime: 0 })\n          }\n        } else {\n          // Go to previous track\n          const newIndex = state.queueIndex - 1\n          const newTrackId = state.queue[newIndex]\n          set({\n            queueIndex: newIndex,\n            currentTrackId: newTrackId,\n            currentTime: 0,\n            playbackState: 'loading',\n          })\n\n          const track = state.tracks[newTrackId]\n          if (track) {\n            audioManager\n              .loadTrack(track)\n              .then(() => {\n                audioManager.play()\n                set({ playbackState: 'playing' })\n              })\n              .catch((error) => {\n                set({\n                  playbackState: 'error',\n                  error: error.message,\n                })\n              })\n          }\n        }\n        get().updateQueueMetadata()\n      },\n\n      playTrackAtIndex(index) {\n        const state = get()\n        if (index >= 0 && index < state.queue.length) {\n          const trackId = state.queue[index]\n          set({\n            queueIndex: index,\n            currentTrackId: trackId,\n            currentTime: 0,\n            playbackState: 'loading',\n          })\n\n          const track = state.tracks[trackId]\n          if (track) {\n            audioManager\n              .loadTrack(track)\n              .then(() => {\n                audioManager.play()\n                set({ playbackState: 'playing' })\n              })\n              .catch((error) => {\n                set({\n                  playbackState: 'error',\n                  error: error.message,\n                })\n              })\n          }\n        }\n        get().updateQueueMetadata()\n      },\n\n      handleTrackEnded() {\n        const state = get()\n        // Repeat one - replay current track\n        if (state.repeatMode === 'one') {\n          audioManager.seek(0)\n          audioManager.play()\n          set({ currentTime: 0, playbackState: 'playing' })\n          return\n        }\n        // Automatically play next track\n        get().nextTrack()\n      },\n\n      // ========================================\n      // Progress\n      // ========================================\n\n      seek(time) {\n        const state = get()\n        if (time >= 0 && time <= state.duration) {\n          audioManager.seek(time)\n          set({ currentTime: time })\n        }\n      },\n\n      updateTime(time) {\n        set({ currentTime: time })\n      },\n\n      updateBuffered(buffered) {\n        set({ buffered: Math.max(0, Math.min(100, buffered)) })\n      },\n\n      setDuration(duration) {\n        set({ duration })\n      },\n\n      // ========================================\n      // Volume\n      // ========================================\n\n      setVolume(volume) {\n        const clampedVolume = Math.max(0, Math.min(1, volume))\n        audioManager.setVolume(clampedVolume)\n\n        // Auto-unmute if volume > 0\n        if (clampedVolume > 0) {\n          audioManager.setMuted(false)\n          set({ volume: clampedVolume, isMuted: false })\n        } else {\n          set({ volume: clampedVolume, isMuted: true })\n        }\n\n        // Persist to localStorage\n        try {\n          localStorage.setItem('player_volume', clampedVolume.toString())\n        } catch {\n          // Ignore\n        }\n      },\n\n      toggleMute() {\n        const state = get()\n        const newIsMuted = !state.isMuted\n        audioManager.setMuted(newIsMuted)\n        set({ isMuted: newIsMuted })\n      },\n\n      // ========================================\n      // Playback Modes\n      // ========================================\n\n      setRepeatMode(mode) {\n        const state = get()\n        set({ repeatMode: mode })\n        get().updateQueueMetadata()\n\n        // Persist to localStorage\n        try {\n          localStorage.setItem('player_repeat_mode', mode)\n        } catch {\n          // Ignore\n        }\n      },\n\n      toggleShuffle() {\n        const state = get()\n        const newShuffleMode = !state.shuffleMode\n        set({ shuffleMode: newShuffleMode })\n\n        if (newShuffleMode) {\n          // Enable shuffle\n          if (state.currentTrackId) {\n            const currentOriginalIndex = state.originalQueue.indexOf(state.currentTrackId)\n            const newQueue = shuffleWithCurrentItem(state.originalQueue, currentOriginalIndex)\n            set({ queue: newQueue, queueIndex: 0 })\n          } else {\n            set({ queue: shuffle(state.originalQueue) })\n          }\n        } else {\n          // Disable shuffle - restore original order\n          const restoredQueue = [...state.originalQueue]\n          set({ queue: restoredQueue })\n\n          // Find current track in original queue\n          if (state.currentTrackId) {\n            const newIndex = restoredQueue.indexOf(state.currentTrackId)\n            set({ queueIndex: newIndex })\n          }\n        }\n        get().updateQueueMetadata()\n\n        // Persist to localStorage\n        try {\n          localStorage.setItem('player_shuffle', newShuffleMode.toString())\n        } catch {\n          // Ignore\n        }\n      },\n\n      // ========================================\n      // Queue Management\n      // ========================================\n\n      loadPlaylist(playlistId, tracks, startIndex = 0) {\n        const state = get()\n\n        // Store all tracks in entities\n        const newTracks = { ...state.tracks }\n        tracks.forEach((track) => {\n          newTracks[track.id] = track\n        })\n\n        const trackIds = tracks.map((t) => t.id)\n        const newQueue = state.shuffleMode ? shuffle(trackIds) : trackIds\n        const newQueueIndex = Math.max(0, Math.min(startIndex, tracks.length - 1))\n\n        set({\n          tracks: newTracks,\n          currentPlaylistId: playlistId,\n          queue: newQueue,\n          originalQueue: trackIds,\n          queueIndex: newQueueIndex,\n          currentTrackId: null,\n          currentTime: 0,\n          playbackState: 'loading',\n        })\n\n        // Start playing the first track\n        const trackId = newQueue[newQueueIndex]\n        const track = newTracks[trackId]\n        if (track) {\n          audioManager\n            .loadTrack(track)\n            .then(() => {\n              audioManager.play()\n              set({ playbackState: 'playing' })\n            })\n            .catch((error) => {\n              set({\n                playbackState: 'error',\n                error: error.message,\n              })\n            })\n        }\n        get().updateQueueMetadata()\n      },\n\n      addToQueue(tracks) {\n        const state = get()\n\n        // Store tracks in entities\n        const newTracks = { ...state.tracks }\n        tracks.forEach((track) => {\n          newTracks[track.id] = track\n        })\n\n        const trackIds = tracks.map((t) => t.id)\n        const newOriginalQueue = [...state.originalQueue, ...trackIds]\n\n        let newQueue: string[]\n        if (state.shuffleMode) {\n          // In shuffle mode, add tracks in random positions\n          const shuffledNew = shuffle(trackIds)\n          newQueue = [...state.queue, ...shuffledNew]\n        } else {\n          newQueue = [...state.queue, ...trackIds]\n        }\n\n        set({\n          tracks: newTracks,\n          originalQueue: newOriginalQueue,\n          queue: newQueue,\n        })\n        get().updateQueueMetadata()\n      },\n\n      insertNext(track) {\n        const state = get()\n\n        // Store track in entities\n        const newTracks = { ...state.tracks, [track.id]: track }\n        const currentOriginalIndex = state.originalQueue.indexOf(state.currentTrackId || '')\n\n        // Insert after current track in both queues\n        const newOriginalQueue = [...state.originalQueue]\n        const newQueue = [...state.queue]\n\n        newOriginalQueue.splice(currentOriginalIndex + 1, 0, track.id)\n        newQueue.splice(state.queueIndex + 1, 0, track.id)\n\n        set({\n          tracks: newTracks,\n          originalQueue: newOriginalQueue,\n          queue: newQueue,\n        })\n        get().updateQueueMetadata()\n      },\n\n      removeFromQueue(index) {\n        const state = get()\n        if (index < 0 || index >= state.queue.length) return\n\n        const newQueue = [...state.queue]\n        const trackId = newQueue[index]\n        newQueue.splice(index, 1)\n\n        const newOriginalQueue = [...state.originalQueue]\n        const originalIndex = newOriginalQueue.indexOf(trackId)\n        if (originalIndex !== -1) {\n          newOriginalQueue.splice(originalIndex, 1)\n        }\n\n        // Adjust queue index\n        let newQueueIndex = state.queueIndex\n        if (index < state.queueIndex) {\n          newQueueIndex--\n        } else if (index === state.queueIndex) {\n          // Removing current track - play next\n          if (newQueue.length > 0) {\n            if (newQueueIndex >= newQueue.length) {\n              newQueueIndex = newQueue.length - 1\n            }\n          } else {\n            newQueueIndex = -1\n          }\n        }\n\n        set({\n          queue: newQueue,\n          originalQueue: newOriginalQueue,\n          queueIndex: newQueueIndex,\n        })\n        get().updateQueueMetadata()\n      },\n\n      clearQueue() {\n        set({\n          queue: [],\n          originalQueue: [],\n          queueIndex: -1,\n          currentTrackId: null,\n          currentPlaylistId: null,\n          playbackState: 'idle',\n          currentTime: 0,\n          duration: 0,\n        })\n        audioManager.stop()\n        get().updateQueueMetadata()\n      },\n\n      // ========================================\n      // Error Handling\n      // ========================================\n\n      setError(error) {\n        set({\n          error,\n          playbackState: 'error',\n        })\n      },\n\n      clearError() {\n        const state = get()\n        if (state.playbackState === 'error') {\n          set({\n            error: null,\n            playbackState: 'idle',\n          })\n        } else {\n          set({ error: null })\n        }\n      },\n\n      // ========================================\n      // Internal Helpers\n      // ========================================\n\n      updateQueueMetadata() {\n        const state = get()\n        if (state.queue.length === 0) {\n          set({ hasNextTrack: false, hasPreviousTrack: false })\n          return\n        }\n\n        const isAtEnd = state.queueIndex >= state.queue.length - 1\n        const isAtBeginning = state.queueIndex <= 0\n\n        // Has next if not at end, or if repeat mode is on\n        const hasNextTrack = !isAtEnd || state.repeatMode === 'all' || state.repeatMode === 'one'\n\n        // Has previous if not at beginning, or if repeat mode is 'all', or if more than 3 seconds into track\n        const hasPreviousTrack =\n          !isAtBeginning || state.repeatMode === 'all' || state.currentTime > 3\n\n        set({ hasNextTrack, hasPreviousTrack })\n      },\n    }),\n    {\n      name: 'musicfun-player',\n      storage: createJSONStorage(() => localStorage),\n      partialize: (state) => ({\n        volume: state.volume,\n        repeatMode: state.repeatMode,\n        shuffleMode: state.shuffleMode,\n        // Don't persist queue and current track to avoid sync issues\n      }),\n    }\n  )\n)\n\n// ========================================\n// Audio Event Listeners Setup\n// ========================================\n\nlet audioListenersSetup = false\nlet cleanupListeners: (() => void) | null = null\n\nexport function setupAudioListeners() {\n  if (audioListenersSetup) return\n\n  audioListenersSetup = true\n\n  const timeupdateHandler = (time: number) => {\n    usePlayerStore.setState({ currentTime: time })\n  }\n\n  const loadedmetadataHandler = ({ duration }: { duration: number }) => {\n    usePlayerStore.setState({ duration, isLoadingTrack: false })\n  }\n\n  const endedHandler = () => {\n    usePlayerStore.getState().handleTrackEnded()\n  }\n\n  const errorHandler = (error: string) => {\n    usePlayerStore.setState({\n      playbackState: 'error',\n      error,\n      isLoadingTrack: false,\n    })\n  }\n\n  const waitingHandler = () => {\n    usePlayerStore.setState({ isLoadingTrack: true })\n  }\n\n  const canplayHandler = () => {\n    usePlayerStore.setState({ isLoadingTrack: false })\n  }\n\n  audioManager.on('timeupdate', timeupdateHandler)\n  audioManager.on('loadedmetadata', loadedmetadataHandler)\n  audioManager.on('ended', endedHandler)\n  audioManager.on('error', errorHandler)\n  audioManager.on('waiting', waitingHandler)\n  audioManager.on('canplay', canplayHandler)\n\n  // Return cleanup function\n  cleanupListeners = () => {\n    audioManager.off('timeupdate', timeupdateHandler)\n    audioManager.off('loadedmetadata', loadedmetadataHandler)\n    audioManager.off('ended', endedHandler)\n    audioManager.off('error', errorHandler)\n    audioManager.off('waiting', waitingHandler)\n    audioManager.off('canplay', canplayHandler)\n    audioListenersSetup = false\n  }\n}\n\n// Initialize audio listeners on first use\nexport function initializePlayer() {\n  setupAudioListeners()\n}\n\n// Cleanup function for testing or unmount\nexport function cleanupPlayer() {\n  if (cleanupListeners) {\n    cleanupListeners()\n    cleanupListeners = null\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/model/player-track-hooks.ts",
    "content": "import { usePlayerStore } from './player-store'\nimport type { TrackPlaybackState, TrackProgress } from '../types/player.types'\n\n/**\n * Hook that returns playback state for a specific track\n * This ensures only the specific track component rerenders when its state changes\n *\n * Usage:\n * ```tsx\n * const trackState = useTrackPlaybackState(trackId)\n * ```\n */\nexport function useTrackPlaybackState(trackId: string): TrackPlaybackState {\n  return usePlayerStore((state) => ({\n    isCurrentTrack: state.currentTrackId === trackId,\n    isPlaying: state.currentTrackId === trackId && state.playbackState === 'playing',\n    isPaused: state.currentTrackId === trackId && state.playbackState === 'paused',\n    playbackState: state.currentTrackId === trackId ? state.playbackState : ('idle' as const),\n  }))\n}\n\n/**\n * Hook that returns progress for a specific track\n * Only returns progress data if this is the current track\n *\n * Usage:\n * ```tsx\n * const progress = useTrackProgress(trackId)\n * ```\n */\nexport function useTrackProgress(trackId: string): TrackProgress {\n  return usePlayerStore((state) => {\n    if (state.currentTrackId !== trackId) {\n      return { progress: 0, currentTime: 0 }\n    }\n    return {\n      progress: state.duration > 0 ? (state.currentTime / state.duration) * 100 : 0,\n      currentTime: state.currentTime,\n    }\n  })\n}\n\n/**\n * Returns whether a specific track is the current track\n */\nexport function useIsCurrentTrack(trackId: string): boolean {\n  return usePlayerStore((state) => state.currentTrackId === trackId)\n}\n\n/**\n * Returns the position of a track in the queue\n */\nexport function useTrackQueuePosition(trackId: string): number | null {\n  return usePlayerStore((state) => {\n    const index = state.queue.indexOf(trackId)\n    return index === -1 ? null : index\n  })\n}\n\n/**\n * Check if track is in queue\n */\nexport function useIsTrackInQueue(trackId: string): boolean {\n  return usePlayerStore((state) => state.queue.includes(trackId))\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/task.md",
    "content": "### Revised Prompt for AI Assistant:\n\n**Task: Implement a Global Audio Player State Management using Zustand**\n\n**Context:**\nI need to create a robust audio player business logic using **Zustand**.\nThe player should wrap a browser `Audio` instance and manage playback for\na web application that might display hundreds of tracks simultaneously.\n\n**Core Requirements:**\n\n1.  **Singleton Audio Instance:** Only one track can play at a time.\n2.  **Playback Logic:**\n    - `play(track, playlist)`: If it's a new track, start from 0. If it's the current track, resume from the paused moment.\n    - `pause()`: Pause the current track and save the position.\n    - `toggle(track, playlist)`: Smart toggle (play/pause).\n3.  **Playlist Management:**\n    - The store should know about the current playlist context.\n    - When a track ends, automatically play the next one based on the current mode.\n4.  **Playback Modes:**\n    - `repeat`: 'off' | 'one' | 'all'\n    - `shuffle`: boolean.\n5.  **Performance & Scaling:**\n    - The application will render lists with hundreds of tracks.\n    - **Crucial:** Re-renders must be optimized. A track component should only re-render if its specific state (isCurrent, isPlaying) changes, not when the progress bar of another track moves.\n6.  **Missing Requirements to Add:**\n    - **Progress Tracking:** Handle `currentTime` and `duration` updates.\n    - **Volume & Mute:** Global volume state.\n    - **Loading State:** Handle `isLoading` or `isBuffering` states for the audio.\n    - **Error Handling:** Handle cases where the audio source fails to load.\n\n**Technical Specifications:**\n\n- **Zustand Store:**\n  - Define the state interface (Track object, CurrentState, Modes).\n  - Define actions (play, pause, stop, next, previous, setVolume, setModes).\n  - The `Audio` instance should be managed within the store (or a ref/service), ensuring event listeners (`onEnded`, `onTimeUpdate`) are properly synchronized with the state.\n- **Selectors:**\n  - Create efficient hooks or selectors to allow components to subscribe only to the necessary parts of the state.\n- **Business Logic:**\n  - Implement \"Next Track\" logic considering `shuffle` and `repeat` modes.\n  - For `shuffle`, explain how you will handle the queue (e.g., a shuffled array of IDs).\n\n**Expected Output:**\n\n1.  **Zustand Store implementation:** Full code for the store using TypeScript.\n2.  **Performance-optimized Selectors:** Examples of how to use `useStore` with shallow comparisons or specific selectors.\n3.  **React Component Examples:**\n    - `PlayerControls`: A global component for Play/Pause/Next/Prev/Progress/Volume.\n    - `TrackItem`: A high-performance list item component that shows:\n      - Play/Pause button for this specific track.\n      - Visual indication if this track is currently active.\n      - A progress bar that _only_ appears/updates if this specific track is the one playing.\n\n**Focus solely on Business Logic and State Management.** Use a clean, modular approach. Ensure that the `Audio` object is properly cleaned up (listeners removed) to avoid memory leaks.\n\nYou can look at musicfun-react-all-stacks/apps/rtk-query/src/player to get imagination of what I want you do\n\n---\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/types/player.types.ts",
    "content": "export interface Track {\n  id: string\n  title: string\n  artist: string\n  album?: string\n  duration: number // in seconds\n  url: string\n  albumArt?: string\n  artistId?: string\n  albumId?: string\n}\n\nexport interface Playlist {\n  id: string\n  name: string\n  description?: string\n  trackIds: string[]\n  createdAt: string\n  updatedAt: string\n  coverImage?: string\n}\n\nexport type PlaybackState = 'idle' | 'playing' | 'paused' | 'loading' | 'error'\n\nexport type RepeatMode = 'off' | 'one' | 'all'\n\nexport interface PlayerState {\n  // Current playback state\n  currentTrackId: string | null\n  currentPlaylistId: string | null\n  playbackState: PlaybackState\n\n  // Playback position\n  currentTime: number // in seconds\n  duration: number // in seconds\n  buffered: number // percentage 0-100\n\n  // Volume control\n  volume: number // 0-1\n  isMuted: boolean\n\n  // Playback modes\n  repeatMode: RepeatMode\n  shuffleMode: boolean\n\n  // Queue management\n  queue: string[] // ordered track IDs\n  originalQueue: string[] // original order before shuffle\n  queueIndex: number\n\n  // Track entities - normalized storage\n  tracks: Record<string, Track> // tracks stored by ID\n\n  // Error handling\n  error: string | null\n\n  // Additional metadata\n  isLoadingTrack: boolean\n  hasNextTrack: boolean\n  hasPreviousTrack: boolean\n}\n\nexport interface TrackPlaybackState {\n  isCurrentTrack: boolean\n  isPlaying: boolean\n  isPaused: boolean\n  playbackState: PlaybackState\n}\n\nexport interface TrackProgress {\n  progress: number\n  currentTime: number\n}\n\nexport interface FormattedTime {\n  current: string\n  duration: string\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/utils/convert-api-track-to-player-track.ts",
    "content": "import type { ApiTrack } from '@/shared/types/api-track.types.ts'\nimport type { Track } from '../types/player.types'\nimport { getCoverUrl } from '@/shared/utils/get-cover-url'\nimport { getAudioUrl } from '@/shared/utils/get-audio-url'\nimport { getArtistName } from '@/shared/utils/get-artist-name'\nimport { getArtistId } from '@/shared/utils/get-artist-id'\n\n/**\n * Converts API track response to Player Track format\n */\nexport const convertApiTrackToPlayerTrack = (apiTrack: any): Track => {\n  // Extract attributes based on type\n  const attributes = apiTrack.attributes\n\n  // Get user (if available)\n  const user = 'user' in attributes ? attributes.user : undefined\n\n  // Extract audio URL\n  const audioUrl = getAudioUrl(attributes.attachments)\n\n  // Get cover URL\n  const coverUrl = getCoverUrl(attributes.images)\n\n  // Get artist name\n  const artistName = getArtistName(attributes, user)\n\n  // Get artist ID\n  const artistId = getArtistId(apiTrack)\n\n  // Get duration (available in TrackDetailsAttributes, not in TrackListItemOutput)\n  const duration = 'duration' in attributes ? attributes.duration : 0\n\n  return {\n    id: apiTrack.id,\n    title: attributes.title,\n    artist: artistName,\n    duration, // 0 for track lists, actual value for detailed info\n    url: audioUrl, // Critical - player needs audio URL\n    albumArt: coverUrl,\n    artistId: artistId,\n    album: undefined, // Not available in current schema\n  }\n}\n\n/**\n * Converts array of API tracks to Player Track format\n */\nexport const convertApiTracksToPlayerTracks = (apiTracks: any[]): Track[] => {\n  return apiTracks.map(convertApiTrackToPlayerTrack)\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/utils/format-time.ts",
    "content": "/**\n * Formats seconds into MM:SS or HH:MM:SS format\n */\nexport function formatTime(seconds: number): string {\n  if (!isFinite(seconds) || seconds < 0) {\n    return '0:00'\n  }\n\n  const hours = Math.floor(seconds / 3600)\n  const minutes = Math.floor((seconds % 3600) / 60)\n  const secs = Math.floor(seconds % 60)\n\n  if (hours > 0) {\n    return `${hours}:${padZero(minutes)}:${padZero(secs)}`\n  }\n\n  return `${minutes}:${padZero(secs)}`\n}\n\n/**\n * Pads a number with leading zero if less than 10\n */\nfunction padZero(num: number): string {\n  return num.toString().padStart(2, '0')\n}\n\n/**\n * Parses a time string (MM:SS or HH:MM:SS) into seconds\n */\nexport function parseTime(timeString: string): number {\n  const parts = timeString.split(':').map(Number)\n\n  if (parts.length === 2) {\n    // MM:SS\n    return parts[0] * 60 + parts[1]\n  } else if (parts.length === 3) {\n    // HH:MM:SS\n    return parts[0] * 3600 + parts[1] * 60 + parts[2]\n  }\n\n  return 0\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/utils/index.ts",
    "content": "export { shuffle, shuffleWithCurrentItem } from './shuffle'\nexport { formatTime, parseTime } from './format-time'\nexport {\n  convertApiTrackToPlayerTrack,\n  convertApiTracksToPlayerTracks,\n} from './convert-api-track-to-player-track'\nexport { getNextTrackId, getPreviousTrackId, getQueuePosition } from './track-navigation'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/utils/shuffle.ts",
    "content": "/**\n * Fisher-Yates shuffle algorithm\n * Shuffles an array in place and returns it\n */\nexport function shuffle<T>(array: T[]): T[] {\n  const shuffled = [...array]\n\n  for (let i = shuffled.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1))\n    ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]\n  }\n\n  return shuffled\n}\n\n/**\n * Shuffles an array but keeps the current item at its position\n * Useful when enabling shuffle mode while a track is playing\n */\nexport function shuffleWithCurrentItem<T>(array: T[], currentIndex: number): T[] {\n  if (currentIndex < 0 || currentIndex >= array.length) {\n    return shuffle(array)\n  }\n\n  const currentItem = array[currentIndex]\n  const otherItems = array.filter((_, index) => index !== currentIndex)\n  const shuffledOthers = shuffle(otherItems)\n\n  // Insert current item at the beginning\n  return [currentItem, ...shuffledOthers]\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/player/utils/track-navigation.ts",
    "content": "import type { RepeatMode } from '../types/player.types'\n\n/**\n * Утилита для вычисления ID следующего трека в очереди\n */\nexport function getNextTrackId(\n  queue: string[],\n  queueIndex: number,\n  repeatMode: RepeatMode\n): string | null {\n  if (queue.length === 0) return null\n\n  const isAtEnd = queueIndex >= queue.length - 1\n\n  if (isAtEnd) {\n    if (repeatMode === 'one') return queue[queueIndex]\n    if (repeatMode === 'all') return queue[0]\n    return null\n  }\n\n  return queue[queueIndex + 1]\n}\n\n/**\n * Утилита для вычисления ID предыдущего трека в очереди\n */\nexport function getPreviousTrackId(\n  queue: string[],\n  queueIndex: number,\n  repeatMode: RepeatMode\n): string | null {\n  if (queue.length === 0) return null\n\n  const isAtBeginning = queueIndex <= 0\n\n  if (isAtBeginning) {\n    if (repeatMode === 'all') return queue[queue.length - 1]\n    return queue[0]\n  }\n\n  return queue[queueIndex - 1]\n}\n\n/**\n * Утилита для вычисления позиции в очереди\n */\nexport function getQueuePosition(queueIndex: number, queueLength: number) {\n  return {\n    current: queueIndex + 1,\n    total: queueLength,\n    isFirst: queueIndex === 0,\n    isLast: queueIndex >= queueLength - 1,\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/api/client.ts",
    "content": "import createClient, { type Middleware } from 'openapi-fetch'\n\nimport type { paths } from './schema.ts'\n\nconst config = {\n  baseURL: null as string | null,\n  apiKey: null as string | null,\n  getAccessToken: null as (() => Promise<string | null>) | null,\n  saveAccessToken: null as ((accessToken: string | null) => Promise<void>) | null,\n  getRefreshToken: null as (() => Promise<string | null>) | null,\n  saveRefreshToken: null as ((refreshToken: string | null) => Promise<void>) | null,\n  toManyRequestsErrorHandler: null as ((message: string | null) => void) | null,\n  logoutHandler: null as (() => void) | null,\n}\n\nexport const setClientConfig = (newConfig: Partial<typeof config>) => {\n  Object.assign(config, newConfig)\n  _client = undefined // пере-инициализируем\n}\n\nexport const getClientConfig = () => ({ ...config })\n\n/* ------------------------------------------------------------------ */\n/* 2.  Mutex для refresh-а                                             */\n/* ------------------------------------------------------------------ */\nlet refreshPromise: Promise<string> | null = null\n\nexport function makeRefreshToken(): Promise<string> {\n  if (!refreshPromise) {\n    // 1) создаём «замок» сразу\n    refreshPromise = (async (): Promise<string> => {\n      const refreshToken = await config.getRefreshToken!()\n      if (!refreshToken) throw new Error('No refresh token')\n\n      const res = await fetch(`${config.baseURL}/auth/refresh`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'API-KEY': config.apiKey!,\n        },\n        body: JSON.stringify({ refreshToken }),\n      })\n      if (res.status !== 201) throw new Error('Refresh failed')\n\n      const { accessToken, refreshToken: newRT } = await res.json()\n      await config.saveAccessToken!(accessToken)\n      await config.saveRefreshToken!(newRT)\n\n      return accessToken\n    })().finally(() => {\n      refreshPromise = null // 2) снимаем «замок»\n    })\n  }\n\n  return refreshPromise\n}\n\nconst authMiddleware: Middleware = {\n  /* ---------- REQUEST -------------------------------------------------- */\n  async onRequest({ request }) {\n    request.headers.set('API-KEY', config.apiKey!)\n\n    const token = await config.getAccessToken?.()\n    if (token) request.headers.set('Authorization', `Bearer ${token}`)\n    ;(request as any)._retryClone = request.clone()\n\n    return request\n  },\n  async onResponse({ request, response }) {\n    const req = request as Request & { _retry: boolean }\n\n    if (response.status === 429) {\n      const { message } = await response.clone().json()\n      config.toManyRequestsErrorHandler?.(message)\n    }\n\n    if (response.status !== 401 || request.url.includes('/auth/refresh')) {\n      return response // всё ок\n    }\n\n    // уже пытались? -> отдаём 401 наружу, чтобы не зациклиться\n    if (req._retry) return response\n    req._retry = true\n\n    try {\n      const newToken = await makeRefreshToken()\n\n      // повторяем исходный запрос с новым токеном\n      const orig = (req as any)._retryClone as Request // клон с целым body\n      const retry = new Request(orig, { headers: new Headers(orig.headers) })\n      retry.headers.set('Authorization', `Bearer ${newToken}`)\n      return await fetch(retry)\n    } catch (error) {\n      console.log(error)\n      // refresh не удался → чистим хранилище, отдаём 401\n      await config.saveAccessToken!(null)\n      await config.saveRefreshToken!(null)\n      await config.logoutHandler?.()\n      return response\n    }\n  },\n}\n\nlet _client: ReturnType<typeof createClient<paths>> | undefined\n\nconst LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0'])\n\nfunction isLocalClient(): boolean {\n  if (typeof window === 'undefined') return false // не клиент\n  const h = window.location.hostname\n  return LOCAL_HOSTNAMES.has(h) || h.endsWith('.localhost')\n}\n\nexport function assertApiConfig() {\n  if (!config.baseURL) {\n    const msg = 'baseURL is required. Call setClientConfig({ baseURL })'\n    console.error(msg)\n    throw new Error(msg)\n  }\n  if (isLocalClient() && !config.apiKey) {\n    const msg =\n      'apiKey is required when running client on localhost. Call setClientConfig({ apiKey })'\n    console.error(msg)\n    throw new Error(msg)\n  }\n}\n\nexport const getClient = () => {\n  if (_client) return _client\n\n  assertApiConfig()\n\n  const client = createClient<paths>({ baseUrl: config.baseURL! })\n  client.use(authMiddleware)\n  _client = client\n  return _client\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/api/schema.ts",
    "content": "/**\n * This file was auto-generated by openapi-typescript.\n * Do not make direct changes to the file.\n */\n\nexport interface paths {\n  '/playlists/my': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Get my playlists\n     * @deprecated\n     */\n    get: operations['PlaylistsController_getMyPlaylists']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Retrieve all playlists\n     * @description Query parameters must conform to the **GetPlaylistsRequestPayload** schema.\n     */\n    get: operations['PlaylistsPublicController_getPlaylists']\n    put?: never\n    /** Create a new playlist */\n    post: operations['PlaylistsController_createPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get a single playlist by ID */\n    get: operations['PlaylistsPublicController_getPlaylistById']\n    /** Update a playlist */\n    put: operations['PlaylistsController_updatePlaylist']\n    post?: never\n    /** Delete a playlist */\n    delete: operations['PlaylistsController_deletePlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Reorder playlists */\n    put: operations['PlaylistsController_reorderPlaylist']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/images/main': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /**\n     * Upload playlist cover\n     * @description Minimum height — 500px; image must be square\n     */\n    post: operations['PlaylistsController_uploadMainImage']\n    /** Delete playlist cover */\n    delete: operations['PlaylistsController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Get list of all tracks in all playlists.\n     * @description Query-parameters schema → [`GetTracksRequestPayload`](#model-GetTracksRequestPayload)\n     */\n    get: operations['TracksPublicController_getAllTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/count/{userId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get tracks count for a user */\n    get: operations['TracksPublicController_getTracksCount']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get list of tracks in a playlist */\n    get: operations['TracksPublicController_getPlaylistTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get track details by ID */\n    get: operations['TracksPublicController_getTrackDetails']\n    /** Update track information */\n    put: operations['TracksController_updateTrack']\n    post?: never\n    /** Permanently delete a track */\n    delete: operations['TracksController_deleteTrackCompletely']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Like or toggle like on a track */\n    post: operations['TracksPublicController_likeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Dislike or toggle dislike on a track */\n    post: operations['TracksPublicController_dislikeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove user reaction from a track */\n    delete: operations['TracksPublicController_removeTrackReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/count/{userId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get playlists count for a user */\n    get: operations['PlaylistsPublicController_getPlaylistsCount']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Like a playlist */\n    post: operations['PlaylistsPublicController_likePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Dislike a playlist */\n    post: operations['PlaylistsPublicController_dislikePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove user reaction from a playlist */\n    delete: operations['PlaylistsPublicController_removePlaylistReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks/{trackId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Reorder tracks in a playlist */\n    put: operations['TracksController_reorderTrack']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Add a track to your playlist */\n    post: operations['TracksController_addTrackToPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove a track from your playlist */\n    delete: operations['TracksController_unbindTrackFromPlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/actions/publish': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Publish a track (make it publicly available) */\n    post: operations['TracksController_publishTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/cover': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Upload track cover */\n    post: operations['TracksController_uploadTrackCover']\n    /** Delete track cover */\n    delete: operations['TracksController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/upload': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a track with MP3 file upload. Allowed file extensions: mp3, max size: 1 MB */\n    post: operations['TracksController_uploadTrackMp3']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a new artist */\n    post: operations['ArtistsController_createArtist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Search artists by substring */\n    get: operations['ArtistsController_searchArtist']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Delete an artist by ID */\n    delete: operations['ArtistsController_deleteArtist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/oauth-redirect': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * OAuth redirect\n     * @description The callback URL to redirect after granting access, <a target=\"_blank\" href=\"https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\">https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid</a>\n     */\n    get: operations['AuthController_OauthRedirect']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/login': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Log in using the code received after OAuth authorization redirect */\n    post: operations['AuthController_login']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/refresh': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Refresh refresh/access token pair */\n    post: operations['AuthController_refresh']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/logout': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Deactivate refresh token */\n    post: operations['AuthController_logout']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/me': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get current user by access token */\n    get: operations['AuthController_getMe']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/simple/login': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Simple login with login/password, returns access and refresh tokens */\n    post: operations['SimpleAuthController_login']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/simple/refresh': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Simple refresh: rotate access and refresh tokens */\n    post: operations['SimpleAuthController_refresh']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/simple/logout': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Simple logout: revoke refresh token */\n    post: operations['SimpleAuthController_logout']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a new tag */\n    post: operations['TagsController_createTag']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Search tags by substring */\n    get: operations['TagsController_searchTags']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Delete a tag by ID */\n    delete: operations['TagsController_deleteTag']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n}\nexport type webhooks = Record<string, never>\nexport interface components {\n  schemas: {\n    UserRef: {\n      /** @description Unique identifier of the user */\n      id: string\n      /** @description Name of the user */\n      name: string\n    }\n    /**\n     * @description Type of the image size (e.g., original, thumbnail variants)\n     * @enum {string}\n     */\n    ImageSizeType: ImageSizeType\n    ImageVariant: {\n      /** @description Type of the image size (e.g., original, thumbnail variants) */\n      type: components['schemas']['ImageSizeType']\n      /** @description Image width in pixels */\n      width: number\n      /** @description Image height in pixels */\n      height: number\n      /** @description Image file size in bytes */\n      fileSize: number\n      /** @description Full public URL of the image */\n      url: string\n    }\n    PlaylistImagesOutputDTO: {\n      /** @description Original images and thumbnail previews */\n      main?: components['schemas']['ImageVariant'][]\n    }\n    TagRef: {\n      /** @description Unique identifier of the tag */\n      id: string\n      /** @description Original name of the tag */\n      name: string\n    }\n    /**\n     * @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike\n     * @enum {number}\n     */\n    ReactionValue: ReactionValue\n    PlaylistListItemAttributes: {\n      /** @description Title of the playlist */\n      title: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Order index of the playlist */\n      order: number\n      /** @description User who created the playlist */\n      user: components['schemas']['UserRef']\n      /** @description Images associated with the playlist */\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      /** @description Tags linked to the playlist */\n      tags: components['schemas']['TagRef'][]\n      /** @description Total number of likes for this playlist */\n      likesCount: number\n      /** @description Total number of dislikes for this playlist */\n      dislikesCount: number\n      /** @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n      currentUserReaction: components['schemas']['ReactionValue']\n      /** @description Total number of tracks in the playlist */\n      tracksCount: number\n    }\n    PlaylistListItemResource: {\n      /** @description Unique identifier of the playlist */\n      id: string\n      /**\n       * @description Resource type (should be \"playlists\")\n       * @example playlists\n       */\n      type: string\n      attributes: components['schemas']['PlaylistListItemAttributes']\n    }\n    GetMyPlaylistsOutput: {\n      /** @description Array of playlist resource objects owned by the current user */\n      data: components['schemas']['PlaylistListItemResource'][]\n    }\n    CreatePlaylistAttributes: {\n      /** @description Playlist title (1 to 100 characters) */\n      title: string\n      /** @description Playlist description (up to 1000 characters) */\n      description: string | null\n    }\n    CreatePlaylistData: {\n      /** @example playlists */\n      type: string\n      attributes: components['schemas']['CreatePlaylistAttributes']\n    }\n    CreatePlaylistRequestPayload: {\n      data: components['schemas']['CreatePlaylistData']\n    }\n    PlaylistAttributes: {\n      /** @description Title of the playlist */\n      title: string\n      /** @description Description of the playlist */\n      description: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Order index of the playlist */\n      order: number\n      /** @description User who created the playlist */\n      user: components['schemas']['UserRef']\n      /** @description Images associated with the playlist */\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      /** @description Tags linked to the playlist */\n      tags: components['schemas']['TagRef'][]\n      /** @description Total number of likes for this playlist */\n      likesCount: number\n      /** @description Total number of dislikes for this playlist */\n      dislikesCount: number\n      /** @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n      currentUserReaction: components['schemas']['ReactionValue']\n      /** @description Total number of tracks in the playlist */\n      tracksCount: number\n    }\n    PlaylistResource: {\n      /** @description Unique identifier of the playlist */\n      id: string\n      /**\n       * @description Resource type (should be \"playlists\")\n       * @example playlists\n       */\n      type: string\n      attributes: components['schemas']['PlaylistAttributes']\n    }\n    GetPlaylistOutput: {\n      data: components['schemas']['PlaylistResource']\n    }\n    UpdatePlaylistAttributes: {\n      /** @description Playlist title (1 – 100 characters) */\n      title: string\n      /**\n       * @description Playlist description (up to 1000 characters)\n       * @example Cool playlist\n       */\n      description: string | null\n      /** @description Tag IDs to associate with the playlist (0 – 5 items; [] = clear tags) */\n      tagIds: string[]\n    }\n    UpdatePlaylistData: {\n      /** @example playlists */\n      type: string\n      attributes: components['schemas']['UpdatePlaylistAttributes']\n    }\n    UpdatePlaylistRequestPayload: {\n      data: components['schemas']['UpdatePlaylistData']\n    }\n    ReorderPlaylistsRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID of the playlist after which the current playlist should be inserted. Send null to place the playlist at the beginning of the list.\n       */\n      putAfterItemId: string | null\n    }\n    TrackImages: {\n      /** @description List of original images and thumbnail versions (e.g., original, 320x180, etc.) */\n      main?: components['schemas']['ImageVariant'][]\n    }\n    GetTracksRequestPayload: {\n      /**\n       * @description Page number for pagination (starting from 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Page size for pagination (between 1 and 20)\n       * @default 20\n       */\n      pageSize: number\n      /** @description Search term for filtering playlists by name */\n      search?: string\n      /**\n       * @description Field by which to sort tracks\n       * @default publishedAt\n       * @enum {string}\n       */\n      sortBy: PathsPlaylistsTracksGetParametersQuerySortBy\n      /**\n       * @description Sort direction (ascending or descending)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection\n      /** @description Filter by tag IDs (multiple values allowed) */\n      tagsIds?: string[]\n      /** @description Filter by artist IDs (multiple values allowed) */\n      artistsIds?: string[]\n      /** @description Filter by user ID (track creator's ID) */\n      userId?: string\n      /** @description If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n      includeDrafts?: boolean\n      /**\n       * @description Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination.\n       * @default offset\n       * @enum {string}\n       */\n      paginationType: PathsPlaylistsTracksGetParametersQueryPaginationType\n      /** @description Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\". */\n      cursor?: string | null\n    }\n    JsonApiErrorSource: {\n      /**\n       * @description e.g. \"/data/attributes/field\"\n       * @example /data/attributes/field\n       */\n      pointer?: string\n      /**\n       * @description e.g. \"?queryParam\"\n       * @example ?queryParam\n       */\n      parameter?: string\n    }\n    JsonApiError: {\n      /**\n       * @description HTTP status code as a string\n       * @example 404\n       */\n      status: string\n      /**\n       * @description Application-specific error code\n       * @example E123\n       */\n      code?: Record<string, never>\n      /**\n       * @description Short, human-readable summary\n       * @example Not Found\n       */\n      title?: string\n      /**\n       * @description Detailed explanation\n       * @example User with ID 123 not found\n       */\n      detail?: string\n      /** @description Pointer to the associated entity in the request */\n      source?: components['schemas']['JsonApiErrorSource']\n      /** @description Any extra data */\n      meta?: Record<string, never>\n    }\n    JsonApiErrorDocument: {\n      /** @description Array of one or more errors */\n      errors: components['schemas']['JsonApiError'][]\n      /** @description e.g. timestamp, path, traceId, etc. */\n      meta?: Record<string, never>\n    }\n    TrackAttachment: {\n      /** @description Unique identifier of the entity */\n      id: string\n      /**\n       * Format: date-time\n       * @description Date and time when the entity was added\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the entity was last updated\n       */\n      updatedAt: string\n      /** @description Version number of the entity (for concurrency control) */\n      version: number\n      /**\n       * @description Public URL to access the uploaded file\n       * @example https://cdn.example.com/uploads/track123/cover.jpg\n       */\n      url: string\n      /**\n       * @description MIME type of the file\n       * @example image/jpeg\n       */\n      contentType: string\n      /**\n       * @description Original filename uploaded by the user\n       * @example cover.jpg\n       */\n      originalName: string\n      /**\n       * @description Size of the file in bytes\n       * @example 34872\n       */\n      fileSize: number\n    }\n    TrackListItemAttributes: {\n      title: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      likesCount: number\n      attachments: components['schemas']['TrackAttachment'][]\n      images: components['schemas']['TrackImages']\n      user: components['schemas']['UserRef']\n      /**\n       * @description 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк\n       * @enum {number}\n       */\n      currentUserReaction: ReactionValue\n      isPublished: boolean\n      /**\n       * Format: date-time\n       * @description Date and time when the track was published (ISO 8601)\n       */\n      publishedAt?: string | null\n    }\n    ArtistRelationship: {\n      id: string\n      type: string\n    }\n    ArtistsRelationship: {\n      data: components['schemas']['ArtistRelationship'][]\n    }\n    TrackRelationships: {\n      artists: components['schemas']['ArtistsRelationship']\n    }\n    TrackListItemResource: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackListItemAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMetaWithPagingAndCursor: {\n      page: number\n      pageSize: number\n      /** @description Total count may be absent when using keyset pagination */\n      totalCount: number | null\n      /** @description Total number of pages */\n      pagesCount: number | null\n      /** @description Cursor for the next page */\n      nextCursor: string | null\n    }\n    OmitTypeClass: {\n      /** @description Name of the artist */\n      name: string\n    }\n    IncludedArtistOutput: {\n      id: string\n      type: string\n      attributes: components['schemas']['OmitTypeClass']\n    }\n    GetTrackListOutput: {\n      data: components['schemas']['TrackListItemResource'][]\n      meta: components['schemas']['JsonApiMetaWithPagingAndCursor']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    GetTracksCountOutput: {\n      /**\n       * @description Total number of tracks for the user\n       * @example 12\n       */\n      count: number\n    }\n    TrackListItemAttributesForPlaylist: {\n      /** @description Title of the track */\n      title: string\n      /** @description Order index of the track in the playlist */\n      order: number\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Attachments related to the track */\n      attachments: components['schemas']['TrackAttachment'][]\n      /** @description Images associated with the track */\n      images: components['schemas']['TrackImages']\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – liked; -1 – disliked\n       * @enum {number|null}\n       */\n      currentUserReaction: ReactionValue\n      /**\n       * Format: date-time\n       * @description Date and time when the track was published (ISO 8601)\n       */\n      publishedAt?: string | null\n    }\n    TrackListItemResourceForPlaylist: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackListItemAttributesForPlaylist']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMeta: {\n      totalCount: number\n    }\n    GetTracksForPlaylistOutput: {\n      data: components['schemas']['TrackListItemResourceForPlaylist'][]\n      meta: components['schemas']['JsonApiMeta']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    ArtistRef: {\n      /** @description Unique identifier of the artist */\n      id: string\n      /** @description Name of the artist */\n      name: string\n    }\n    TrackDetailsAttributes: {\n      /** @description Track title */\n      title: string\n      /** @description Track lyrics text */\n      lyrics?: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate?: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Duration of the track in seconds */\n      duration: number\n      /** @description Total number of likes for this track */\n      likesCount: number\n      /**\n       * @deprecated\n       * @description Total number of dislikes for this track\n       */\n      dislikesCount: number\n      /** @description List of attachments related to the track */\n      attachments: components['schemas']['TrackAttachment'][]\n      images: components['schemas']['TrackImages']\n      /** @description Tags associated with the track */\n      tags: components['schemas']['TagRef'][]\n      /** @description Artists associated with the track */\n      artists: components['schemas']['ArtistRef'][]\n      user: components['schemas']['UserRef']\n      /** @description Publication status of the track */\n      isPublished: boolean\n      /**\n       * Format: date-time\n       * @description Publication date in ISO 8601 format\n       */\n      publishedAt?: string | null\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n       * @enum {number}\n       */\n      currentUserReaction: ReactionValue\n    }\n    TrackDetailsResource: {\n      /** @description Unique identifier of the track */\n      id: string\n      /**\n       * @description Resource type (should be \"tracks\")\n       * @example tracks\n       */\n      type: string\n      attributes: components['schemas']['TrackDetailsAttributes']\n    }\n    GetTrackDetailsOutput: {\n      data: components['schemas']['TrackDetailsResource']\n    }\n    ReactionOutput: {\n      objectId: string\n      /** @enum {number} */\n      value: ReactionValue\n      likes: number\n      dislikes: number\n    }\n    GetPlaylistsRequestPayload: {\n      /**\n       * @description Page number for pagination (starting from 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Page size for pagination (between 1 and 20)\n       * @default 20\n       */\n      pageSize: number\n      /** @description Search term for filtering playlists by name */\n      search?: string\n      /**\n       * @description Field by which to sort playlists\n       * @default addedAt\n       * @enum {string}\n       */\n      sortBy: PathsPlaylistsGetParametersQuerySortBy\n      /**\n       * @description Sort direction (ascending or descending)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: PathsPlaylistsGetParametersQuerySortDirection\n      /** @description Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n      tagsIds?: string[]\n      /** @description Filter by user ID (playlist creator’s ID) */\n      userId?: string\n      /** @description Filter by track ID – only playlists containing this track will be returned */\n      trackId?: string\n    }\n    JsonApiMetaWithPaging: {\n      totalCount: number\n      page: number\n      pageSize: number\n      pagesCount: number\n    }\n    GetPlaylistsOutput: {\n      /** @description Array of playlist resource objects */\n      data: components['schemas']['PlaylistListItemResource'][]\n      meta: components['schemas']['JsonApiMetaWithPaging']\n    }\n    GetPlaylistsCountOutput: {\n      /**\n       * @description Total number of playlists for the user\n       * @example 5\n       */\n      count: number\n    }\n    ReorderTracksRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID of the track after which the current track should be inserted. Send null to place the track at the beginning of the list.\n       * @example a1b2c3d4-e5f6-7890-abcd-1234567890ef\n       */\n      putAfterItemId: string | null\n    }\n    UpdateTrackAttributes: {\n      /** @description Track title (1 to 100 characters) */\n      title: string\n      /** @description Track lyrics (up to 5000 characters) */\n      lyrics: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate: string | null\n      /** @description Array of tag IDs to associate with the track (up to 5) */\n      tagIds: string[]\n      /** @description Array of artist IDs to associate with the track (up to 5) */\n      artistsIds: string[]\n    }\n    UpdateTrackData: {\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['UpdateTrackAttributes']\n    }\n    UpdateTrackRequestPayload: {\n      data: components['schemas']['UpdateTrackData']\n    }\n    AddTrackToPlaylistAttributes: {\n      /** @description ID of the track to add to the playlist */\n      trackId: string\n    }\n    AddTrackToPlaylistData: {\n      /** @example playlist-tracks */\n      type: string\n      attributes: components['schemas']['AddTrackToPlaylistAttributes']\n    }\n    AddTrackToPlaylistRequestPayload: {\n      data: components['schemas']['AddTrackToPlaylistData']\n    }\n    CreateArtistAttributes: {\n      /** @description Artist name (must be between 2 and 30 characters) */\n      name: string\n    }\n    CreateArtistData: {\n      /** @example artists */\n      type: string\n      attributes: components['schemas']['CreateArtistAttributes']\n    }\n    CreateArtistRequestPayload: {\n      data: components['schemas']['CreateArtistData']\n    }\n    LoginRequestPayload: {\n      /** @description Authorization code received from OAuth server after redirect */\n      code: string\n      /**\n       * @description Specify the same redirect URI used in the initial OAuth server request\n       * @example http://localhost:3000/oauth2/callback\n       */\n      redirectUri: string\n      /**\n       * @description Access token lifetime (default \"3m\"); must be a string like \"60s\", \"3m\", \"2h\", or \"1d\"\n       * @example 3m\n       */\n      accessTokenTTL?: string\n      /** @description Refresh token lifetime: if true, 30 days; if false, 30 minutes. accessTokenTTL must not exceed the refresh token lifetime */\n      rememberMe: boolean\n    }\n    RefreshOutput: {\n      refreshToken: string\n      accessToken: string\n    }\n    BadRequestException: Record<string, never>\n    UnauthorizedException: Record<string, never>\n    RefreshRequestPayload: {\n      refreshToken: string\n    }\n    LogoutRequestPayload: {\n      refreshToken: string\n    }\n    GetMeOutput: {\n      userId: string\n      login: string\n    }\n    SimpleLoginRequestPayload: {\n      /** @description User login */\n      login: string\n      /** @description User password */\n      password: string\n      /**\n       * @description Refresh token lifetime: if true, 7 days; if false, 30 minutes\n       * @default true\n       */\n      rememberMe: boolean\n    }\n    SimpleLoginOutput: {\n      accessToken: string\n      refreshToken: string\n    }\n    SimpleRefreshRequestPayload: {\n      /** @description Refresh token */\n      refreshToken: string\n    }\n    SimpleLogoutRequestPayload: {\n      /** @description Refresh token to invalidate */\n      refreshToken: string\n    }\n    CreateTagAttributes: {\n      /** @description Tag name (2 to 30 characters) */\n      name: string\n    }\n    CreateTagData: {\n      /** @example tags */\n      type: string\n      attributes: components['schemas']['CreateTagAttributes']\n    }\n    CreateTagRequestPayload: {\n      data: components['schemas']['CreateTagData']\n    }\n    TagAttributes: {\n      /** @description Original name of the tag */\n      name: string\n    }\n    TagResource: {\n      /** @description Unique identifier of the tag */\n      id: string\n      /**\n       * @description Resource type (should be \"tags\")\n       * @example tags\n       */\n      type: string\n      attributes: components['schemas']['TagAttributes']\n    }\n    GetTagOutput: {\n      data: components['schemas']['TagResource']\n    }\n    GetTagsOutput: {\n      /** @description Array of tag resource objects */\n      data: components['schemas']['TagResource'][]\n    }\n    /**\n     * Format: binary\n     * @description Файл в multipart/form-data\n     */\n    BinaryFile: string\n  }\n  responses: never\n  parameters: never\n  requestBodies: never\n  headers: never\n  pathItems: never\n}\nexport type SchemaUserRef = components['schemas']['UserRef']\nexport type SchemaImageVariant = components['schemas']['ImageVariant']\nexport type SchemaPlaylistImagesOutputDto = components['schemas']['PlaylistImagesOutputDTO']\nexport type SchemaTagRef = components['schemas']['TagRef']\nexport type SchemaPlaylistListItemAttributes = components['schemas']['PlaylistListItemAttributes']\nexport type SchemaPlaylistListItemResource = components['schemas']['PlaylistListItemResource']\nexport type SchemaGetMyPlaylistsOutput = components['schemas']['GetMyPlaylistsOutput']\nexport type SchemaCreatePlaylistAttributes = components['schemas']['CreatePlaylistAttributes']\nexport type SchemaCreatePlaylistData = components['schemas']['CreatePlaylistData']\nexport type SchemaCreatePlaylistRequestPayload =\n  components['schemas']['CreatePlaylistRequestPayload']\nexport type SchemaPlaylistAttributes = components['schemas']['PlaylistAttributes']\nexport type SchemaPlaylistResource = components['schemas']['PlaylistResource']\nexport type SchemaGetPlaylistOutput = components['schemas']['GetPlaylistOutput']\nexport type SchemaUpdatePlaylistAttributes = components['schemas']['UpdatePlaylistAttributes']\nexport type SchemaUpdatePlaylistData = components['schemas']['UpdatePlaylistData']\nexport type SchemaUpdatePlaylistRequestPayload =\n  components['schemas']['UpdatePlaylistRequestPayload']\nexport type SchemaReorderPlaylistsRequestPayload =\n  components['schemas']['ReorderPlaylistsRequestPayload']\nexport type SchemaTrackImages = components['schemas']['TrackImages']\nexport type SchemaGetTracksRequestPayload = components['schemas']['GetTracksRequestPayload']\nexport type SchemaJsonApiErrorSource = components['schemas']['JsonApiErrorSource']\nexport type SchemaJsonApiError = components['schemas']['JsonApiError']\nexport type SchemaJsonApiErrorDocument = components['schemas']['JsonApiErrorDocument']\nexport type SchemaTrackAttachment = components['schemas']['TrackAttachment']\nexport type SchemaTrackListItemAttributes = components['schemas']['TrackListItemAttributes']\nexport type SchemaArtistRelationship = components['schemas']['ArtistRelationship']\nexport type SchemaArtistsRelationship = components['schemas']['ArtistsRelationship']\nexport type SchemaTrackRelationships = components['schemas']['TrackRelationships']\nexport type SchemaTrackListItemResource = components['schemas']['TrackListItemResource']\nexport type SchemaJsonApiMetaWithPagingAndCursor =\n  components['schemas']['JsonApiMetaWithPagingAndCursor']\nexport type SchemaOmitTypeClass = components['schemas']['OmitTypeClass']\nexport type SchemaIncludedArtistOutput = components['schemas']['IncludedArtistOutput']\nexport type SchemaGetTrackListOutput = components['schemas']['GetTrackListOutput']\nexport type SchemaGetTracksCountOutput = components['schemas']['GetTracksCountOutput']\nexport type SchemaTrackListItemAttributesForPlaylist =\n  components['schemas']['TrackListItemAttributesForPlaylist']\nexport type SchemaTrackListItemResourceForPlaylist =\n  components['schemas']['TrackListItemResourceForPlaylist']\nexport type SchemaJsonApiMeta = components['schemas']['JsonApiMeta']\nexport type SchemaGetTracksForPlaylistOutput = components['schemas']['GetTracksForPlaylistOutput']\nexport type SchemaArtistRef = components['schemas']['ArtistRef']\nexport type SchemaTrackDetailsAttributes = components['schemas']['TrackDetailsAttributes']\nexport type SchemaTrackDetailsResource = components['schemas']['TrackDetailsResource']\nexport type SchemaGetTrackDetailsOutput = components['schemas']['GetTrackDetailsOutput']\nexport type SchemaReactionOutput = components['schemas']['ReactionOutput']\nexport type SchemaGetPlaylistsRequestPayload = components['schemas']['GetPlaylistsRequestPayload']\nexport type SchemaJsonApiMetaWithPaging = components['schemas']['JsonApiMetaWithPaging']\nexport type SchemaGetPlaylistsOutput = components['schemas']['GetPlaylistsOutput']\nexport type SchemaGetPlaylistsCountOutput = components['schemas']['GetPlaylistsCountOutput']\nexport type SchemaReorderTracksRequestPayload = components['schemas']['ReorderTracksRequestPayload']\nexport type SchemaUpdateTrackAttributes = components['schemas']['UpdateTrackAttributes']\nexport type SchemaUpdateTrackData = components['schemas']['UpdateTrackData']\nexport type SchemaUpdateTrackRequestPayload = components['schemas']['UpdateTrackRequestPayload']\nexport type SchemaAddTrackToPlaylistAttributes =\n  components['schemas']['AddTrackToPlaylistAttributes']\nexport type SchemaAddTrackToPlaylistData = components['schemas']['AddTrackToPlaylistData']\nexport type SchemaAddTrackToPlaylistRequestPayload =\n  components['schemas']['AddTrackToPlaylistRequestPayload']\nexport type SchemaCreateArtistAttributes = components['schemas']['CreateArtistAttributes']\nexport type SchemaCreateArtistData = components['schemas']['CreateArtistData']\nexport type SchemaCreateArtistRequestPayload = components['schemas']['CreateArtistRequestPayload']\nexport type SchemaLoginRequestPayload = components['schemas']['LoginRequestPayload']\nexport type SchemaRefreshOutput = components['schemas']['RefreshOutput']\nexport type SchemaBadRequestException = components['schemas']['BadRequestException']\nexport type SchemaUnauthorizedException = components['schemas']['UnauthorizedException']\nexport type SchemaRefreshRequestPayload = components['schemas']['RefreshRequestPayload']\nexport type SchemaLogoutRequestPayload = components['schemas']['LogoutRequestPayload']\nexport type SchemaGetMeOutput = components['schemas']['GetMeOutput']\nexport type SchemaSimpleLoginRequestPayload = components['schemas']['SimpleLoginRequestPayload']\nexport type SchemaSimpleLoginOutput = components['schemas']['SimpleLoginOutput']\nexport type SchemaSimpleRefreshRequestPayload = components['schemas']['SimpleRefreshRequestPayload']\nexport type SchemaSimpleLogoutRequestPayload = components['schemas']['SimpleLogoutRequestPayload']\nexport type SchemaCreateTagAttributes = components['schemas']['CreateTagAttributes']\nexport type SchemaCreateTagData = components['schemas']['CreateTagData']\nexport type SchemaCreateTagRequestPayload = components['schemas']['CreateTagRequestPayload']\nexport type SchemaTagAttributes = components['schemas']['TagAttributes']\nexport type SchemaTagResource = components['schemas']['TagResource']\nexport type SchemaGetTagOutput = components['schemas']['GetTagOutput']\nexport type SchemaGetTagsOutput = components['schemas']['GetTagsOutput']\nexport type SchemaBinaryFile = components['schemas']['BinaryFile']\nexport type $defs = Record<string, never>\nexport interface operations {\n  PlaylistsController_getMyPlaylists: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of playlists retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMyPlaylistsOutput']\n        }\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylists: {\n    parameters: {\n      query?: {\n        /** @description Page number for pagination (starting from 1) */\n        pageNumber?: number\n        /** @description Page size for pagination (between 1 and 20) */\n        pageSize?: number\n        /** @description Search term for filtering playlists by name */\n        search?: string\n        /** @description Field by which to sort playlists */\n        sortBy?: PathsPlaylistsGetParametersQuerySortBy\n        /** @description Sort direction (ascending or descending) */\n        sortDirection?: PathsPlaylistsGetParametersQuerySortDirection\n        /** @description Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n        tagsIds?: string[]\n        /** @description Filter by user ID (playlist creator’s ID) */\n        userId?: string\n        /** @description Filter by track ID – only playlists containing this track will be returned */\n        trackId?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: JSON:API list of playlists with pagination */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistsOutput']\n        }\n      }\n    }\n  }\n  PlaylistsController_createPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Playlist created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Forbidden: Playlist creation limit exceeded */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylistById: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the playlist */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Playlist retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Not Found: Playlist with the given ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_updatePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Playlist updated successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Bad Request: Validation error (e.g., tag limit exceeded) */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: You do not have permission to update this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deletePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Playlist deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Insufficient permissions to delete this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_reorderPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderPlaylistsRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Playlist order updated successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist or putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_uploadMainImage: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @description Maximum size 1 MB; minimum height 500px; image must be square */\n          file: components['schemas']['BinaryFile']\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Cover uploaded successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['TrackImages']\n        }\n      }\n      /** @description Bad Request: Invalid image format or dimensions */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No permission to upload cover for this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Cover deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Removing another user’s playlist cover is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getAllTracks: {\n    parameters: {\n      query?: {\n        /** @description Page number for pagination (starting from 1) */\n        pageNumber?: number\n        /** @description Page size for pagination (between 1 and 20) */\n        pageSize?: number\n        /** @description Search term for filtering playlists by name */\n        search?: string\n        /** @description Field by which to sort tracks */\n        sortBy?: PathsPlaylistsTracksGetParametersQuerySortBy\n        /** @description Sort direction (ascending or descending) */\n        sortDirection?: PathsPlaylistsGetParametersQuerySortDirection\n        /** @description Filter by tag IDs (multiple values allowed) */\n        tagsIds?: string[]\n        /** @description Filter by artist IDs (multiple values allowed) */\n        artistsIds?: string[]\n        /** @description Filter by user ID (track creator's ID) */\n        userId?: string\n        /** @description If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n        includeDrafts?: boolean\n        /** @description Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination. */\n        paginationType?: PathsPlaylistsTracksGetParametersQueryPaginationType\n        /** @description Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\". */\n        cursor?: string | null\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Paginated list of tracks */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackListOutput']\n        }\n      }\n      /** @description Bad Request: invalid query parameters */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['JsonApiErrorDocument']\n        }\n      }\n    }\n  }\n  TracksPublicController_getTracksCount: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the user */\n        userId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Tracks count retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTracksCountOutput']\n        }\n      }\n    }\n  }\n  TracksPublicController_getPlaylistTracks: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the playlist to retrieve tracks for */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of tracks in the playlist */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTracksForPlaylistOutput']\n        }\n      }\n      /** @description Not Found: Playlist with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getTrackDetails: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the track to retrieve details for */\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Track details with attachments */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description Not Found: Track with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_updateTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdateTrackRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Track updated successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description Bad Request: Tag or artist limit exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Editing another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track or playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCompletely: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track permanently deleted */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Deleting another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_likeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: User reaction recorded and counters updated */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid track ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_dislikeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: User reaction recorded and counters updated */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid track ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_removeTrackReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Reaction removed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylistsCount: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the user */\n        userId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Playlists count retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistsCountOutput']\n        }\n      }\n    }\n  }\n  PlaylistsPublicController_likePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: Like recorded successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid playlist ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_dislikePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: Dislike recorded successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid playlist ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_removePlaylistReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Reaction removed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_reorderTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderTracksRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Track order updated successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Bad Request: Cannot place a track after itself */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track or putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_addTrackToPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['AddTrackToPlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Track added to the playlist successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist or track limit exceeded (max 10 tracks) */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_unbindTrackFromPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track removed from the playlist */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_publishTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track published successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Publishing another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Track is already published */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the track for which the cover is being uploaded */\n        trackId: string\n      }\n      cookie?: never\n    }\n    /**\n     * @description Image file:<br/>\n     *             • Field name — <code>cover</code><br/>\n     *             • Allowed MIME types — <code>image/jpeg</code>, <code>image/png</code>, <code>image/gif</code><br/>\n     *             • Maximum size — <code>100 KB</code>\n     */\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** Format: binary */\n          cover: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Cover uploaded successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['TrackImages']\n        }\n      }\n      /** @description Bad Request: Invalid file or size exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Cannot upload a cover for another user’s track */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Cover deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Removing another user's track cover is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackMp3: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @example My cool track */\n          title: string\n          /** Format: binary */\n          file: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Track created successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description Bad Request: Invalid file format or file size exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Internal Server Error: Error saving file or track */\n      500: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_createArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateArtistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Artist created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ArtistRef']\n        }\n      }\n      /** @description Bad Request: Validation error or invalid input */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Limit of 100 artists per user reached */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Artist with the given name already exists */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_searchArtist: {\n    parameters: {\n      query: {\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of artists matching the search */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ArtistRef'][]\n        }\n      }\n    }\n  }\n  ArtistsController_deleteArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Artist deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Artist is attached to tracks or was created by another user */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Artist with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_OauthRedirect: {\n    parameters: {\n      query: {\n        /**\n         * @description The callback URL to redirect after grand access,\n         *          https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\n         */\n        callbackUrl: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Redirect executed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_login: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LoginRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Token pair retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Bad Request: Invalid request format or required parameters are missing */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['BadRequestException']\n        }\n      }\n      /** @description Unauthorized: Code is invalid, expired, missing, or redirectUri does not match */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_refresh: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['RefreshRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Token pair refreshed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Unauthorized: Refresh token is invalid, expired, or missing */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_logout: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LogoutRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Refresh token deactivated; access token remains valid. */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_getMe: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Successfully retrieved user information */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMeOutput']\n        }\n      }\n      /** @description Unauthorized: access token is missing or invalid */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  SimpleAuthController_login: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['SimpleLoginRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Token pair retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['SimpleLoginOutput']\n        }\n      }\n      /** @description Unauthorized: Invalid login or password */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  SimpleAuthController_refresh: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['SimpleRefreshRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Token pair refreshed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['SimpleLoginOutput']\n        }\n      }\n      /** @description Unauthorized: Refresh token is invalid, expired, or revoked */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  SimpleAuthController_logout: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['SimpleLogoutRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Refresh token has been revoked */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_createTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateTagRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Tag created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput']\n        }\n      }\n      /** @description Bad Request: Validation error */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Limit of 100 tags per user reached */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Tag with the given name already exists */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_searchTags: {\n    parameters: {\n      query: {\n        /** @description Substring to search tags by (using normalized name) */\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of matching tags */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagsOutput']\n        }\n      }\n      /** @description Bad Request: Invalid search query */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_deleteTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the tag to delete */\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Tag deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Tag was created by another user or is attached to tracks or playlists */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Tag with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n}\nexport enum PathsPlaylistsGetParametersQuerySortBy {\n  addedAt = 'addedAt',\n  likesCount = 'likesCount',\n}\nexport enum PathsPlaylistsGetParametersQuerySortDirection {\n  asc = 'asc',\n  desc = 'desc',\n}\nexport enum PathsPlaylistsTracksGetParametersQuerySortBy {\n  publishedAt = 'publishedAt',\n  likesCount = 'likesCount',\n}\nexport enum PathsPlaylistsTracksGetParametersQueryPaginationType {\n  offset = 'offset',\n  cursor = 'cursor',\n}\nexport enum ImageSizeType {\n  original = 'original',\n  thumbnail = 'thumbnail',\n  medium = 'medium',\n}\nexport enum ReactionValue {\n  Value0 = 0,\n  Value1 = 1,\n  ValueMinus1 = -1,\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/api/types.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\nexport type MainImage = components['schemas']['ImageVariant'][]\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/api/utils/json-api-error.ts",
    "content": "export interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport type ExtractError<T> = T extends { error?: infer E } ? E : unknown\n\n/* --- типы ошибок, совпадающие с фильтром -------------------------------- */\nexport interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport function isJsonApiErrorDocument(error: unknown): error is JsonApiErrorDocument {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    // @ts-expect-error type no matter\n    Array.isArray(error.errors)\n  )\n}\n\nexport function parseJsonApiErrors(errorDoc: JsonApiErrorDocument): {\n  fieldErrors: Record<string, string>\n  globalErrors: string[]\n} {\n  const fieldErrors: Record<string, string> = {}\n  const globalErrors: string[] = []\n\n  for (const err of errorDoc.errors) {\n    const msg = err.detail ?? err.title ?? 'Unknown error'\n    const ptr = err.source?.pointer\n    if (ptr) {\n      // убираем префикс JSON:API\n      const field = ptr.replace(/^\\/data\\/attributes\\//, '')\n      fieldErrors[field] = msg\n    } else {\n      globalErrors.push(msg)\n    }\n  }\n\n  return { fieldErrors, globalErrors }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/api/utils/unwrap.ts",
    "content": "// types/api.ts\n\nimport { type ExtractError } from './json-api-error.ts'\n\n//-----------------------------------------------------------------------------\n// utils/requestWrapper.ts\n//-----------------------------------------------------------------------------\n// «Умный» обёртчик: Infers Data и Error из P,\n// возвращает Promise<Data>, а в случае ошибки — throw Error\nexport type ExtractData<T> = T extends { data?: infer D } ? NonNullable<D> : never\n\nexport async function unwrap<P extends Promise<{ data?: unknown; error?: unknown }>>(\n  promise: P\n): Promise<ExtractData<Awaited<P>>> {\n  const res = (await promise) as Awaited<P>\n  if ((res as { error?: unknown }).error) {\n    // здесь E = ExtractError<Awaited<P>>\n    throw (res as { error: ExtractError<Awaited<P>> }).error\n  }\n  return (res as { data: ExtractData<Awaited<P>> }).data\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/auth/types/local-storage.keys.ts",
    "content": ""
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/AudioPlayer/AudioPlayer.module.css",
    "content": ".player {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  width: 100%;\n  min-height: 64px;\n\n  background: var(--color-bg-primary);\n}\n\n.trackInfo {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  min-width: 200px;\n}\n\n.cover {\n  width: 112px;\n  height: 112px;\n  border-radius: 4px;\n  background: var(--color-bg-card);\n}\n\n.cover img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.info {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.playerControls {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 8px;\n  align-items: center;\n}\n\n.controls {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n}\n\n.playPauseButton {\n  width: 48px;\n  height: 48px;\n}\n\n.active {\n  color: var(--color-accent);\n}\n\n.iconButton.active:hover,\n.iconButton.active:focus {\n  color: var(--color-accent);\n}\n\n.progressBar {\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  max-width: 632px;\n}\n\n.time {\n  min-width: 36px;\n  font-size: var(--font-size-xs);\n  color: var(--color-text-secondary);\n  text-align: center;\n}\n\n.progress {\n  cursor: pointer;\n\n  height: 5px;\n  border: none;\n  border-radius: 4px;\n\n  accent-color: var(--color-text-primary);\n}\n\n.trackProgress {\n  width: 100%;\n  max-width: 550px;\n}\n\n.volumeColumn {\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n\n  min-width: 160px;\n  padding-right: 32px;\n}\n\n.volumeProgress {\n  width: 119px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/AudioPlayer/AudioPlayer.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport s from '@/widgets/Player/Player.module.css'\n\nimport { AudioPlayer } from './AudioPlayer.tsx'\n\nconst meta = {\n  title: 'Components/Player',\n  component: AudioPlayer,\n  parameters: {},\n  args: {},\n} satisfies Meta<typeof AudioPlayer>\n\nexport default meta\n\nconst demoTrack = {\n  src: 'https://cdn.uppbeat.io/audio-files/c636d7c86452449b1203fc0bded83e29/4358717fc9da477a52fb18a6cbd3afcc/d154b5ce5ff1a05ae8115a3c678062e8/STREAMING-dreamland-matrika-main-version-31140-02-25.mp3',\n  cover: 'https://unsplash.it/112/112',\n  title: 'Play It Safe',\n  artist: 'Julia Wolf',\n}\n\nexport const Basic = {\n  render: () => {\n    const [isPlaying, setIsPlaying] = useState(false)\n    const [isShuffle, setIsShuffle] = useState(false)\n    const [isRepeat, setIsRepeat] = useState(false)\n\n    const [track] = useState(demoTrack)\n    return (\n      <AudioPlayer\n        {...track}\n        onNext={() => {}}\n        onPrevious={() => {}}\n        isShuffle={isShuffle}\n        isRepeat={isRepeat}\n        onShuffle={() => setIsShuffle(!isShuffle)}\n        onRepeat={() => setIsRepeat(!isRepeat)}\n        className={s.player}\n      />\n    )\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/AudioPlayer/AudioPlayer.tsx",
    "content": "import { clsx } from 'clsx'\nimport * as React from 'react'\n\nimport {\n  useCurrentTrack,\n  usePlaybackProgress,\n  usePlaybackState,\n  usePlayerControls,\n  useVolumeControl,\n} from '@/player'\nimport { CoverImage } from '@/shared/components'\nimport { useThrottleCallback } from '@/shared/hooks'\nimport {\n  PauseIcon,\n  PlayIcon,\n  RepeatIcon,\n  ShuffleIcon,\n  SkipNextIcon,\n  SkipPreviousIcon,\n  VolumeIcon,\n  VolumeMuteIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './AudioPlayer.module.css'\n\nexport type PlayerProps = {\n  onNext: () => void\n  onPrevious: () => void\n  isShuffle: boolean\n  isRepeat: boolean\n  onShuffle: () => void\n  onRepeat: () => void\n} & React.ComponentProps<'div'>\n\nconst durationSliderCoefficients = 10\n\nexport const AudioPlayer = ({\n  onNext,\n  onPrevious,\n  isShuffle,\n  isRepeat,\n  onShuffle,\n  onRepeat,\n  className,\n  ...props\n}: PlayerProps) => {\n  const { track: currentTrack } = useCurrentTrack()\n  const { isPlaying } = usePlaybackState()\n  const { currentTime, duration, formattedTime } = usePlaybackProgress()\n  const { volume, isMuted } = useVolumeControl()\n  const { play, pause, seek, setVolume, toggleMute } = usePlayerControls()\n\n  const handlePlayPause = () => {\n    if (isPlaying) {\n      pause()\n    } else {\n      if (currentTrack) play(currentTrack)\n    }\n  }\n\n  const setThrottledTime = useThrottleCallback((time) => {\n    seek(time)\n  }, 15)\n\n  const handleChangeTime = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const time = Number(e.target.value) / durationSliderCoefficients\n    setThrottledTime(time)\n  }\n\n  const setThrottledVolume = useThrottleCallback((newVolume) => {\n    setVolume(newVolume)\n  }, 15)\n\n  const handleVolume = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newVolume = Number(e.target.value)\n    setThrottledVolume(newVolume)\n  }\n\n  const handleVolumeMute = () => {\n    toggleMute()\n  }\n\n  if (!currentTrack) {\n    return null\n  }\n\n  return (\n    <div className={clsx(s.player, className)} {...props}>\n      <div className={s.trackInfo}>\n        <div className={s.cover}>\n          <CoverImage imageSrc={currentTrack.albumArt} imageDescription={'cover'} />\n        </div>\n        <div className={s.info}>\n          <Typography variant=\"body1\" as=\"h3\">\n            {currentTrack.title}\n          </Typography>\n          <Typography variant=\"body2\" as=\"p\">\n            {currentTrack.artist}\n          </Typography>\n        </div>\n      </div>\n\n      <div className={s.playerControls}>\n        <div className={s.controls}>\n          <IconButton onClick={onShuffle} className={clsx(s.iconButton, isShuffle && s.active)}>\n            <ShuffleIcon />\n          </IconButton>\n          <IconButton onClick={onPrevious}>\n            <SkipPreviousIcon />\n          </IconButton>\n          <IconButton className={s.playPauseButton} onClick={handlePlayPause}>\n            {isPlaying ? <PauseIcon /> : <PlayIcon />}\n          </IconButton>\n          <IconButton onClick={onNext}>\n            <SkipNextIcon />\n          </IconButton>\n          <IconButton onClick={onRepeat} className={clsx(s.iconButton, isRepeat && s.active)}>\n            <RepeatIcon />\n          </IconButton>\n        </div>\n\n        <div className={s.progressBar}>\n          <span className={s.time}>{formattedTime.current}</span>\n          <input\n            type=\"range\"\n            min={0}\n            max={duration * durationSliderCoefficients}\n            value={currentTime * durationSliderCoefficients}\n            onChange={handleChangeTime}\n            className={clsx(s.progress, s.trackProgress)}\n          />\n          <span className={s.time}>{formattedTime.duration}</span>\n        </div>\n      </div>\n\n      <div className={s.volumeColumn}>\n        <IconButton onClick={handleVolumeMute}>\n          {isMuted || volume === 0 ? <VolumeMuteIcon /> : <VolumeIcon />}\n        </IconButton>\n        <input\n          type=\"range\"\n          min={0}\n          max={1}\n          step={0.01}\n          value={volume}\n          onChange={handleVolume}\n          className={clsx(s.progress, s.volumeProgress)}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/AudioPlayer/index.ts",
    "content": "export * from './AudioPlayer.tsx'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Autocomplete/Autocomplete.module.css",
    "content": ".container {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.labelError {\n  color: var(--color-text-error);\n}\n\n.inputWrapper {\n  position: relative;\n\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  align-items: flex-start;\n\n  min-height: 48px;\n  padding: 4px 32px 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.inputWrapper:hover:not(.disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.inputWrapper.focused {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-primary);\n}\n\n.inputWrapper.error {\n  border-color: var(--color-text-error);\n}\n\n.inputWrapper.disabled {\n  cursor: not-allowed;\n  background-color: var(--color-disabled);\n}\n\n.tag {\n  cursor: pointer;\n\n  display: flex;\n  gap: 4px;\n  align-items: center;\n  justify-content: center;\n\n  max-width: 120px;\n  padding: 2px 8px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 16px;\n\n  background-color: #2f2f2f;\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  overflow: hidden;\n\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.inputContainer {\n  display: flex;\n  gap: 5px;\n  align-items: center;\n\n  width: 100%;\n  min-height: 40px;\n  padding: 0 12px;\n  border: 1px solid #b3b3b3;\n  border-radius: 4px;\n\n  transition: border-color 0.2s ease;\n}\n\n.input {\n  flex: 1;\n\n  border: none;\n\n  font-family: inherit;\n  font-size: 14px;\n  color: var(--color-text-primary);\n\n  background: transparent;\n  outline: none;\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.dropdownIcon {\n  cursor: pointer;\n\n  position: absolute;\n  z-index: 2;\n  right: 8px;\n  bottom: 4px;\n\n  width: 20px;\n  height: 20px;\n\n  color: var(--color-text-secondary);\n\n  transition: transform 200ms ease;\n}\n\n.dropdownIcon:hover {\n  color: var(--color-text-primary);\n}\n\n.dropdownIconOpen {\n  transform: rotate(180deg);\n}\n\n.dropdown {\n  position: absolute;\n  z-index: 50;\n  top: 100%;\n  left: 0;\n\n  overflow-y: auto;\n\n  width: 100%;\n  max-height: 200px;\n  margin-top: 4px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 4px;\n\n  background-color: #2d2d2d;\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n\n  animation: dropdown-show 200ms ease-out;\n}\n\n.option {\n  cursor: pointer;\n\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  padding: 8px 12px;\n\n  transition: all 200ms ease;\n}\n\n.optionFocused:not(.optionDisabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.optionDisabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.noResults {\n  padding: 12px;\n  text-align: center;\n}\n\n.noResultsText {\n  color: var(--color-text-secondary);\n}\n\n.errorMessage {\n  margin-top: 4px;\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.counter {\n  margin-top: 4px;\n  color: var(--color-text-secondary);\n}\n\n/* Animations */\n@keyframes dropdown-show {\n  from {\n    transform: translateY(-4px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n.selected {\n  background: #51173c;\n}\n\n.tagsWrapper {\n  cursor: default;\n\n  position: relative;\n\n  display: flex;\n  flex: 1;\n  flex-wrap: nowrap;\n  gap: 4px;\n  align-content: flex-start;\n\n  min-width: 90%;\n  min-height: 24px;\n}\n\n.underlinedPart {\n  cursor: pointer;\n\n  display: inline;\n\n  font: inherit;\n  color: inherit;\n  text-decoration: underline;\n\n  background: none;\n}\n\n.hiddenTagsBlock {\n  position: absolute;\n  z-index: 10;\n  top: -50px;\n  left: 0;\n\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n\n  margin-top: 4px;\n  padding: 8px;\n  border: 1px solid var(--color-text-secondary);\n  border-radius: 4px;\n\n  background: black;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Autocomplete/Autocomplete.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from '../Dialog'\nimport { Typography } from '../Typography'\nimport { Autocomplete, type AutocompleteOption } from './Autocomplete'\n\nconst meta = {\n  title: 'Components/Autocomplete',\n  component: Autocomplete,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Autocomplete>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n// Sample data\nconst programmingLanguages: AutocompleteOption[] = [\n  { value: 'javascript', label: 'JavaScript' },\n  { value: 'typescript', label: 'TypeScript' },\n  { value: 'python', label: 'Python' },\n  { value: 'java', label: 'Java' },\n  { value: 'cpp', label: 'C++' },\n  { value: 'csharp', label: 'C#' },\n  { value: 'php', label: 'PHP' },\n  { value: 'ruby', label: 'Ruby' },\n  { value: 'go', label: 'Go' },\n  { value: 'rust', label: 'Rust' },\n  { value: 'kotlin', label: 'Kotlin' },\n  { value: 'swift', label: 'Swift' },\n]\n\nconst musicGenres: AutocompleteOption[] = [\n  { value: 'rock', label: 'Rock' },\n  { value: 'pop', label: 'Pop' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hiphop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n  { value: 'blues', label: 'Blues' },\n  { value: 'reggae', label: 'Reggae' },\n  { value: 'folk', label: 'Folk' },\n  { value: 'metal', label: 'Metal' },\n  { value: 'indie', label: 'Indie' },\n]\n\nconst skills: AutocompleteOption[] = [\n  { value: 'frontend', label: 'Frontend Development' },\n  { value: 'backend', label: 'Backend Development' },\n  { value: 'fullstack', label: 'Full Stack Development' },\n  { value: 'mobile', label: 'Mobile Development' },\n  { value: 'devops', label: 'DevOps' },\n  { value: 'testing', label: 'Testing & QA' },\n  { value: 'design', label: 'UI/UX Design' },\n  { value: 'pm', label: 'Project Management', disabled: true },\n  { value: 'data', label: 'Data Science' },\n  { value: 'ml', label: 'Machine Learning' },\n]\n\nexport const Basic = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Programming Languages\"\n          placeholder=\"Search and select languages...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (max 3)\"\n          placeholder=\"Choose up to 3 genres...\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          maxTags={3}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithPreselected = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>(['javascript', 'typescript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Your Skills\"\n          placeholder=\"Add more skills...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithDisabledOptions = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Skills & Roles\"\n          placeholder=\"Select your skills...\"\n          options={skills}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>(['rock', 'jazz'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (disabled)\"\n          placeholder=\"Cannot select\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          disabled\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithError = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Required Skills\"\n          placeholder=\"Select at least one skill...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          errorMessage=\"Please select at least one programming language\"\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendSkills, setFrontendSkills] = useState<string[]>(['javascript'])\n    const [backendSkills, setBackendSkills] = useState<string[]>([])\n    const [genres, setGenres] = useState<string[]>([])\n\n    const frontendOptions: AutocompleteOption[] = [\n      { value: 'html', label: 'HTML' },\n      { value: 'css', label: 'CSS' },\n      { value: 'javascript', label: 'JavaScript' },\n      { value: 'typescript', label: 'TypeScript' },\n      { value: 'react', label: 'React' },\n      { value: 'vue', label: 'Vue.js' },\n      { value: 'angular', label: 'Angular' },\n      { value: 'svelte', label: 'Svelte' },\n    ]\n\n    const backendOptions: AutocompleteOption[] = [\n      { value: 'nodejs', label: 'Node.js' },\n      { value: 'python', label: 'Python' },\n      { value: 'java', label: 'Java' },\n      { value: 'csharp', label: 'C#' },\n      { value: 'php', label: 'PHP' },\n      { value: 'ruby', label: 'Ruby' },\n      { value: 'go', label: 'Go' },\n      { value: 'rust', label: 'Rust' },\n    ]\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Developer Profile Setup\n          </Typography>\n        </div>\n\n        <Autocomplete\n          label=\"Frontend Technologies\"\n          placeholder=\"Select frontend skills...\"\n          options={frontendOptions}\n          value={frontendSkills}\n          onChange={setFrontendSkills}\n          maxTags={5}\n        />\n\n        <Autocomplete\n          label=\"Backend Technologies\"\n          placeholder=\"Select backend skills...\"\n          options={backendOptions}\n          value={backendSkills}\n          onChange={setBackendSkills}\n          maxTags={4}\n        />\n\n        <Autocomplete\n          label=\"Favorite Music Genres\"\n          placeholder=\"What music do you like?\"\n          options={musicGenres}\n          value={genres}\n          onChange={setGenres}\n          maxTags={6}\n        />\n\n        <Card style={{ padding: '16px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Profile Summary\n          </Typography>\n\n          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>\n            <Typography variant=\"body2\">\n              <strong>Frontend:</strong>{' '}\n              {frontendSkills.length > 0 ? frontendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Backend:</strong>{' '}\n              {backendSkills.length > 0 ? backendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Music:</strong> {genres.length > 0 ? genres.join(', ') : 'None'}\n            </Typography>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => {\n    const [state1, setState1] = useState<string[]>([])\n    const [state2, setState2] = useState<string[]>(['rock', 'jazz'])\n    const [state3, setState3] = useState<string[]>([])\n    const [state4, setState4] = useState<string[]>(['javascript'])\n\n    return (\n      <div\n        style={{\n          width: '600px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '32px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Empty State\n          </Typography>\n          <Autocomplete\n            label=\"Programming Languages\"\n            placeholder=\"Start typing to search...\"\n            options={programmingLanguages}\n            value={state1}\n            onChange={setState1}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Selected Values\n          </Typography>\n          <Autocomplete\n            label=\"Music Genres\"\n            placeholder=\"Add more genres...\"\n            options={musicGenres}\n            value={state2}\n            onChange={setState2}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Error\n          </Typography>\n          <Autocomplete\n            label=\"Required Field\"\n            placeholder=\"This field is required\"\n            options={programmingLanguages}\n            value={state3}\n            onChange={setState3}\n            errorMessage=\"Please select at least one option\"\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Disabled State\n          </Typography>\n          <Autocomplete\n            label=\"Locked Selection\"\n            placeholder=\"Cannot modify\"\n            options={programmingLanguages}\n            value={state4}\n            onChange={setState4}\n            disabled\n          />\n        </div>\n      </div>\n    )\n  },\n}\n\nexport const InDialog = {\n  render: () => {\n    const [isOpen, setIsOpen] = useState(false)\n    const [selectedSkills, setSelectedSkills] = useState<string[]>([])\n    const [selectedGenres, setSelectedGenres] = useState<string[]>(['rock'])\n\n    const handleSubmit = () => {\n      console.log('Selected skills:', selectedSkills)\n      console.log('Selected genres:', selectedGenres)\n      setIsOpen(false)\n    }\n\n    const handleReset = () => {\n      setSelectedSkills([])\n      setSelectedGenres([])\n    }\n\n    return (\n      <>\n        <Button onClick={() => setIsOpen(true)}>Open Profile Settings</Button>\n\n        <Dialog open={isOpen} onClose={() => setIsOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Edit Your Profile</Typography>\n            <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n              Update your skills and music preferences\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '24px',\n                minWidth: '400px',\n              }}>\n              <Autocomplete\n                label=\"Technical Skills\"\n                placeholder=\"Search and select your skills...\"\n                options={skills}\n                value={selectedSkills}\n                onChange={setSelectedSkills}\n                maxTags={8}\n              />\n\n              <Autocomplete\n                label=\"Favorite Music Genres\"\n                placeholder=\"What music do you enjoy?\"\n                options={musicGenres}\n                value={selectedGenres}\n                onChange={setSelectedGenres}\n                maxTags={5}\n              />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset All\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setIsOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={handleSubmit}>\n              Save Profile\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Autocomplete/Autocomplete.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { useGetId } from '@/shared/hooks'\nimport {\n  ArrowDownIcon,\n  SearchIcon,\n  CheckedIcon,\n  UncheckedIcon,\n  DeleteTagIconButton,\n} from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './Autocomplete.module.css'\nimport { useTranslation } from 'react-i18next'\n\nexport type AutocompleteOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type AutocompleteProps = {\n  label?: ReactNode\n  placeholder?: string\n  options: AutocompleteOption[]\n  value: string[]\n  searchTerm?: string\n  setSearchTerm?: (value: string) => void\n  onChange: (value: string[]) => void\n  disabled?: boolean\n  maxTags?: number\n  errorMessage?: string\n  className?: string\n  isRenderInPortal?: boolean\n} & Omit<ComponentProps<'div'>, 'onChange'>\n\nexport const Autocomplete = ({\n  label,\n  placeholder,\n  options,\n  value,\n  searchTerm: externalSearchTerm,\n  setSearchTerm: externalSetSearchTerm,\n  onChange,\n  disabled = false,\n  maxTags,\n  errorMessage,\n  className,\n  isRenderInPortal = false,\n  ...props\n}: AutocompleteProps) => {\n  const { t } = useTranslation()\n  const [internalSearchTerm, setInternalSearchTerm] = useState('')\n  const [isOpen, setIsOpen] = useState(false)\n  const [focusedIndex, setFocusedIndex] = useState(-1)\n  const [isPopupOpen, setIsPopupOpen] = useState(false)\n  const hiddenTagsBlockRef = useRef<HTMLDivElement>(null)\n\n  const searchTerm = externalSearchTerm !== undefined ? externalSearchTerm : internalSearchTerm\n  const setSearchTerm = externalSetSearchTerm || setInternalSearchTerm\n\n  const containerRef = useRef<HTMLDivElement>(null)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const inputWrapperRef = useRef<HTMLDivElement>(null)\n  const dropdownRef = useRef<HTMLDivElement | null>(null)\n\n  const id = useGetId(props.id)\n\n  const filteredOptions = options\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n  const showError = Boolean(errorMessage)\n\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as Node\n      if (containerRef.current && !containerRef.current.contains(target)) {\n        if (isRenderInPortal) {\n          if (\n            inputWrapperRef.current &&\n            !inputWrapperRef.current.contains(target) &&\n            dropdownRef.current &&\n            !dropdownRef.current.contains(target)\n          ) {\n            setIsOpen(false)\n            setFocusedIndex(-1)\n          }\n        } else {\n          setIsOpen(false)\n          setFocusedIndex(-1)\n        }\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen, isRenderInPortal])\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (disabled) return\n\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault()\n        if (!isOpen) {\n          setIsOpen(true)\n          setFocusedIndex(0)\n        } else {\n          setFocusedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : prev))\n        }\n        break\n\n      case 'ArrowUp':\n        e.preventDefault()\n        setFocusedIndex((prev) => (prev > 0 ? prev - 1 : 0))\n        break\n\n      case 'Enter':\n        e.preventDefault()\n        if (isOpen && focusedIndex >= 0 && filteredOptions[focusedIndex]) {\n          toggleOption(filteredOptions[focusedIndex])\n        }\n        break\n\n      case 'Escape':\n        e.preventDefault()\n        setIsOpen(false)\n        setFocusedIndex(-1)\n        break\n    }\n  }\n\n  const toggleOption = (option: AutocompleteOption) => {\n    if (option.disabled) return\n\n    const isSelected = value.includes(option.value)\n\n    if (isSelected) {\n      onChange(value.filter((v) => v !== option.value))\n    } else if (!isMaxTagsReached) {\n      onChange([...value, option.value])\n    }\n  }\n\n  const removeTag = (tagValue: string) => {\n    onChange(value.filter((v) => v !== tagValue))\n  }\n\n  const handleInputFocus = () => {\n    if (!disabled) {\n      setIsOpen(true)\n    }\n  }\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setSearchTerm(e.target.value)\n    setIsOpen(true)\n    setFocusedIndex(-1)\n  }\n\n  const selectedOptions = options.filter((option) => value.includes(option.value))\n\n  const maxVisibleTags = 2\n  const visibleTags = selectedOptions.slice(0, maxVisibleTags)\n  const hiddenTagsCount = selectedOptions.length - maxVisibleTags\n  const hiddenTags = selectedOptions.slice(maxVisibleTags)\n\n  useEffect(() => {\n    if (isPopupOpen && hiddenTagsBlockRef.current) {\n      hiddenTagsBlockRef.current.focus()\n    }\n  }, [isPopupOpen])\n\n  return (\n    <div className={clsx(s.container, className)} ref={containerRef} {...props}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          className={clsx(s.label, showError && s.labelError)}\n          as=\"label\"\n          htmlFor={id}>\n          {label}\n        </Typography>\n      )}\n\n      <div\n        className={clsx(\n          s.inputWrapper,\n          isOpen && s.focused,\n          showError && s.error,\n          disabled && s.disabled\n        )}\n        ref={inputWrapperRef}>\n        <div className={s.tagsWrapper}>\n          {visibleTags.map((option) => (\n            <div key={option.value} className={s.tag} title={option.label}>\n              <Typography variant=\"body2\" className={s.tagText} as=\"label\">\n                #{option.label}\n              </Typography>\n\n              {!disabled && (\n                <IconButton\n                  onClick={() => removeTag(option.value)}\n                  className={s.deleteButton}\n                  type=\"button\"\n                  tabIndex={-1}>\n                  <DeleteTagIconButton />\n                </IconButton>\n              )}\n            </div>\n          ))}\n\n          {hiddenTagsCount > 0 && (\n            <div className={s.hidenTags}>\n              <Typography variant=\"body2\" className={s.tagText}>\n                and{' '}\n                <button\n                  className={s.underlinedPart}\n                  onClick={(e) => {\n                    e.stopPropagation()\n                    setIsPopupOpen(!isPopupOpen)\n                  }}>\n                  {hiddenTagsCount} more\n                </button>\n              </Typography>\n            </div>\n          )}\n        </div>\n        {isPopupOpen && hiddenTagsCount > 0 && (\n          <div\n            className={s.hiddenTagsBlock}\n            ref={hiddenTagsBlockRef}\n            tabIndex={0}\n            autoFocus\n            onBlur={(e) => {\n              if (!e.currentTarget.contains(e.relatedTarget as Node)) {\n                setIsPopupOpen(false)\n              }\n            }}\n            onKeyDown={(e) => {\n              if (e.key === 'Escape') {\n                setIsPopupOpen(false)\n              }\n            }}>\n            {hiddenTags.map((option) => (\n              <div key={option.value} className={s.tag} title={option.label}>\n                <Typography variant=\"body2\" className={s.tagText} as=\"label\">\n                  #{option.label}\n                </Typography>\n\n                {!disabled && (\n                  <IconButton\n                    onClick={() => removeTag(option.value)}\n                    className={s.deleteButton}\n                    type=\"button\"\n                    tabIndex={-1}>\n                    <DeleteTagIconButton />\n                  </IconButton>\n                )}\n              </div>\n            ))}\n          </div>\n        )}\n\n        <div className={s.inputContainer}>\n          <SearchIcon width={20} height={20} />\n          <input\n            id={id}\n            ref={inputRef}\n            type=\"text\"\n            className={s.input}\n            value={searchTerm}\n            onChange={handleInputChange}\n            onFocus={handleInputFocus}\n            onKeyDown={handleKeyDown}\n            placeholder={placeholder || t('placeholder.search_and_select')}\n            disabled={disabled || isMaxTagsReached}\n            autoComplete=\"off\"\n          />\n        </div>\n        <ArrowDownIcon\n          className={clsx(s.dropdownIcon, isOpen && s.dropdownIconOpen)}\n          onClick={() => !disabled && setIsOpen(!isOpen)}\n        />\n      </div>\n\n      {isOpen && !disabled && (\n        <div className={s.dropdown}>\n          {filteredOptions.length > 0 ? (\n            filteredOptions.map((option, index) => {\n              const isSelected = value.includes(option.value)\n\n              return (\n                <div\n                  key={option.value}\n                  role=\"option\"\n                  aria-selected={isSelected}\n                  aria-disabled={option.disabled}\n                  className={clsx(\n                    s.option,\n                    index === focusedIndex && s.optionFocused,\n                    option.disabled && s.optionDisabled,\n                    isSelected && s.selected\n                  )}\n                  onMouseEnter={() => setFocusedIndex(index)}\n                  onMouseDown={(e) => e.preventDefault()}\n                  onClick={() => !option.disabled && toggleOption(option)}\n                  onMouseLeave={() => setFocusedIndex(-1)}>\n                  {isSelected ? <CheckedIcon /> : <UncheckedIcon />}\n                  <Typography variant=\"body2\">#{option.label}</Typography>\n                </div>\n              )\n            })\n          ) : (\n            <div className={s.noResults}>\n              <Typography variant=\"body2\" className={s.noResultsText}>\n                {searchTerm\n                  ? t('placeholder.no_options_found')\n                  : t('placeholder.all_options_selected')}\n              </Typography>\n            </div>\n          )}\n        </div>\n      )}\n\n      {showError && (\n        <Typography variant=\"error\" className={s.errorMessage}>\n          {errorMessage}\n        </Typography>\n      )}\n\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} {t('placeholder.selected')}\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Autocomplete/index.ts",
    "content": "export * from './Autocomplete'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Avatar/Avatar.module.css",
    "content": ".avatar {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  border-radius: 50%;\n\n  background-color: rgb(15 15 15 / 80%);\n  color: var(--color-text-primary);\n  font-size: var(--font-size-xl);\n  font-weight: 700;\n\n  overflow: hidden;\n}\n\n.avatar > img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.initials {\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Avatar/Avatar.tsx",
    "content": "import { clsx } from 'clsx'\n\nimport { getUserInitials } from '@/shared/utils/get-user-initials'\n\nimport s from './Avatar.module.css'\n\ntype FullName = {\n  name?: string\n  surname?: string\n}\n\ntype AvatarProps = {\n  src?: string | null\n  fullName?: FullName\n  userLogin?: string\n  className?: string\n}\n\nexport const Avatar = ({ src, fullName, userLogin, className }: AvatarProps) => {\n  const initials = getUserInitials(fullName, userLogin)\n\n  return (\n    <div className={clsx(s.avatar, className)}>\n      {src ? <img src={src} alt=\"User avatar\" /> : <span className={s.initials}>{initials}</span>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Avatar/index.ts",
    "content": "export * from './Avatar'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Button/Button.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: inline-flex;\n  gap: 4px;\n  align-items: center;\n  justify-content: center;\n\n  height: 40px;\n  padding: 8px 16px;\n  border-radius: 45px;\n\n  font-size: var(--font-size-s);\n  font-weight: 600;\n  color: var(--color-text-primary);\n\n  transition: opacity 200ms;\n}\n\n.button:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.button:disabled {\n  cursor: initial;\n  background-color: var(--color-disabled);\n}\n\n.button:hover:not(:disabled),\n.button:focus:not(:disabled) {\n  opacity: 0.8;\n}\n\n.primary {\n  background-color: var(--color-accent);\n}\n\n.secondary {\n  background-color: var(--color-bg-interactive-secondary);\n}\n\n.fullWidth {\n  width: 100%;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Button/Button.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Button } from './Button'\n\nconst meta = {\n  title: 'Components/Button',\n  component: Button,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Button>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllButtons: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '24px',\n        flexDirection: 'column',\n        alignItems: 'center',\n        width: '250px',\n      }}>\n      <Button variant=\"primary\">Primary</Button>\n      <Button variant=\"secondary\">Secondary</Button>\n      <Button fullWidth>Full Width</Button>\n      <Button disabled>Disabled</Button>\n      <Button variant=\"primary\" as=\"p\" href=\"https://it-incubator.io/\" target=\"_blank\">\n        Link\n      </Button>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Button/Button.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Button.module.css'\n\nexport type ButtonVariant = 'primary' | 'secondary'\n\nexport type ButtonProps<T extends ElementType = 'button'> = {\n  as?: T\n  fullWidth?: boolean\n  variant?: ButtonVariant\n} & ComponentProps<T>\n\nexport const Button = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  className,\n  fullWidth = false,\n  variant = 'primary',\n  ...props\n}: ButtonProps<T>) => {\n  const classNames = clsx(s.button, s[variant], fullWidth && s.fullWidth, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Button/index.ts",
    "content": "export * from './Button'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Card/Card.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  padding: 8px;\n  background: var(--color-bg-card);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Card/Card.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from '../Typography'\nimport { Card } from './Card'\n\nconst meta = {\n  title: 'Components/Card',\n  component: Card,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Card>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  render: () => (\n    <Card>\n      <Typography variant=\"h2\">Chill Mix</Typography>\n      <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n        Julia Wolf, Khalid, ayokay and more\n      </Typography>\n    </Card>\n  ),\n}\n\nexport const AsSection: Story = {\n  render: () => (\n    <Card as=\"section\">\n      <Typography variant=\"h3\">Card as section</Typography>\n      <Typography variant=\"caption\">You can use any tag via 'as' prop</Typography>\n    </Card>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Card/Card.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType, ReactNode } from 'react'\n\nimport s from './Card.module.css'\n\nexport type CardProps<T extends ElementType = 'div'> = {\n  as?: T\n  className?: string\n  children?: ReactNode\n} & ComponentProps<T>\n\nexport const Card = <T extends ElementType = 'div'>({\n  as: Component = 'div',\n  className,\n  children,\n  ...props\n}: CardProps<T>) => {\n  return (\n    <Component className={clsx(s.card, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Card/index.ts",
    "content": "export * from './Card'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/CoverImage/CoverImage.styles.module.scss",
    "content": ".coverImage {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  display: block;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/CoverImage/CoverImage.tsx",
    "content": "import clsx from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport noCoverPlaceholder from '@/assets/img/no-cover-placeholder.avif'\n\nimport s from './CoverImage.styles.module.scss'\n\ntype Props = {\n  imageSrc?: string\n  imageDescription: string\n} & ComponentProps<'img'>\n\nexport const CoverImage = ({ imageSrc, imageDescription, ...props }: Props) => {\n  return (\n    <div className={clsx(s.coverImage, props.className)}>\n      <img src={imageSrc || noCoverPlaceholder} alt={imageDescription} {...props} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/CoverImage/index.ts",
    "content": "export * from './CoverImage.tsx'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Dialog/Dialog.module.css",
    "content": ".backdrop {\n  position: fixed;\n  z-index: 1;\n  inset: 0;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  background-color: rgb(0 0 0 / 50%);\n\n  animation: fade-in 200ms ease-out;\n}\n\n.dialog {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n\n  max-width: 745px;\n  max-height: 90vh;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-secondary);\n\n  animation: slide-in 200ms ease-out;\n}\n\n.header {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  padding: 18px 24px;\n}\n\n.closeButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: 16px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.closeButton:hover {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  overflow-y: auto;\n  flex: 1;\n  padding: 20px 24px;\n}\n\n.footer {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 8px;\n  padding: 18px 24px;\n}\n\n/* Animations */\n@keyframes fade-in {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes slide-in {\n  from {\n    transform: translateY(-500px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n/* Responsive */\n@media (width <= 768px) {\n  .dialog {\n    max-width: 95vw;\n    margin: 20px;\n  }\n\n  .header,\n  .content,\n  .footer {\n    padding-right: 16px;\n    padding-left: 16px;\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Dialog/Dialog.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from './index'\n\nconst meta = {\n  title: 'Components/Dialog',\n  component: Dialog,\n  parameters: {\n    layout: 'centered',\n  },\n} satisfies Meta<typeof Dialog>\n\nexport default meta\n\nexport const BasicDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Open Basic Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Dialog Title</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <Typography variant=\"body1\">\n              This is dialog content. Here can be any content - text, forms, images and much more.\n            </Typography>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Confirm\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const FormDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Form Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Sign in to Spotifun</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '16px',\n                minWidth: '320px',\n              }}>\n              <TextField label=\"Email or username\" placeholder=\"Enter email or username\" />\n              <TextField label=\"Password\" type=\"password\" placeholder=\"Enter password\" />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Continue without signing in\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Sign in with API/HUB\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const WithoutCloseButton = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog without close button</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader showCloseButton={false}>\n            <Typography variant=\"h2\">Millions of songs.</Typography>\n            <Typography variant=\"body1\" style={{ color: 'var(--color-text-secondary)' }}>\n              Free on Musicfun.\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ textAlign: 'center', padding: '20px 0' }}>\n              <div\n                style={{\n                  width: '60px',\n                  height: '60px',\n                  borderRadius: '50%',\n                  backgroundColor: 'var(--color-accent)',\n                  margin: '0 auto 16px',\n                  display: 'flex',\n                  alignItems: 'center',\n                  justifyContent: 'center',\n                  fontSize: '24px',\n                }}>\n                😊\n              </div>\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '12px',\n                width: '100%',\n              }}>\n              <Button variant=\"primary\" fullWidth onClick={() => setOpen(false)}>\n                Sign up with API/HUB\n              </Button>\n              <Button variant=\"secondary\" fullWidth onClick={() => setOpen(false)}>\n                Continue without signing in\n              </Button>\n            </div>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const LongContent = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog with long content</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Long Content</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ maxWidth: '500px' }}>\n              {Array.from({ length: 20 }, (_, i) => (\n                <Typography key={i} variant=\"body2\" style={{ marginBottom: '12px' }}>\n                  This is paragraph number {i + 1}. Lorem ipsum dolor sit amet, consectetur\n                  adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna\n                  aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\n                </Typography>\n              ))}\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Close\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Dialog/Dialog.tsx",
    "content": "import { clsx } from 'clsx'\nimport { createContext, type ReactNode, use, useEffect } from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { IconButton } from '../IconButton'\nimport s from './Dialog.module.css'\n\ntype DialogContextType = {\n  onClose?: () => void\n}\n\nconst DialogContext = createContext<DialogContextType | null>(null)\n\nconst useDialogContext = () => {\n  const context = use(DialogContext)\n  if (!context) {\n    throw new Error('Dialog compound components must be used within Dialog component')\n  }\n  return context\n}\n\nexport type DialogProps = {\n  children: ReactNode\n  open: boolean\n  onClose?: () => void\n  className?: string\n}\n\nexport const Dialog = ({ children, open, onClose, className }: DialogProps) => {\n  const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (e.target === e.currentTarget) {\n      onClose?.()\n    }\n  }\n\n  // Add global keydown handler for ESC key\n  useEffect(() => {\n    if (!open) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose?.()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [open, onClose])\n\n  if (!open) return null\n\n  const dialogContent = (\n    <div className={s.backdrop} onClick={handleBackdropClick} role=\"dialog\" aria-modal=\"true\">\n      <section className={clsx(s.dialog, className)}>\n        <DialogContext value={{ onClose }}>{children}</DialogContext>\n      </section>\n    </div>\n  )\n\n  return createPortal(dialogContent, document.body)\n}\n\n/*\n * DialogHeader\n */\n\nexport type DialogHeaderProps = {\n  children?: ReactNode\n  className?: string\n  showCloseButton?: boolean\n}\n\nexport const DialogHeader = ({\n  children,\n  className,\n  showCloseButton = true,\n}: DialogHeaderProps) => {\n  const { onClose } = useDialogContext()\n\n  return (\n    <header className={clsx(s.header, className)}>\n      <div>{children}</div>\n      {showCloseButton && (\n        <IconButton onClick={onClose} aria-label=\"Close dialog\" type=\"button\">\n          ✕\n        </IconButton>\n      )}\n    </header>\n  )\n}\n\n/*\n * DialogContent\n */\n\nexport type DialogContentProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogContent = ({ children, className }: DialogContentProps) => {\n  return <div className={clsx(s.content, className)}>{children}</div>\n}\n\n/*\n * DialogFooter\n */\n\nexport type DialogFooterProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogFooter = ({ children, className }: DialogFooterProps) => {\n  return <footer className={clsx(s.footer, className)}>{children}</footer>\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Dialog/index.ts",
    "content": "export * from './Dialog'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/DropdownMenu/DropdownMenu.module.scss",
    "content": ".container {\n  position: relative;\n  display: inline-block;\n}\n\n.trigger {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.trigger:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.trigger:enabled:hover,\n.trigger:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  position: absolute;\n  z-index: 50;\n\n  min-width: 160px;\n  padding: 4px;\n  border-radius: 8px;\n\n  background-color: var(--color-bg-primary);\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n}\n\n.content.align-start {\n  transform-origin: top left;\n}\n\n.content.align-center {\n  transform-origin: top center;\n  transform: translateX(-50%);\n}\n\n.content.align-end {\n  transform-origin: top right;\n  transform: translateX(-100%);\n}\n\n.content.side-top {\n  transform-origin: bottom;\n}\n\n.content.side-top.align-center {\n  transform: translateX(-50%) translateY(-100%);\n}\n\n.content.side-top.align-end {\n  transform: translateX(-100%) translateY(-100%);\n}\n\n.content.side-top.align-start {\n  transform: translateY(-100%);\n}\n\n.item {\n  cursor: pointer;\n\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: none;\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-align: left;\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.item:focus-visible {\n  background-color: var(--color-accent);\n  outline: none;\n}\n\n.item:hover:not(:disabled) {\n  background-color: var(--color-accent);\n}\n\n.itemDisabled {\n  cursor: not-allowed;\n  color: var(--color-text-secondary);\n  opacity: 0.5;\n}\n\n.itemDisabled:hover {\n  background: transparent;\n}\n\n.separator {\n  height: 1px;\n  margin: 4px 0;\n  background-color: var(--color-border-base);\n}\n\n/* Animations */\n@keyframes dropdown-menu-show {\n  from {\n    transform: scale(0.95);\n    opacity: 0;\n  }\n\n  to {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { CreateIcon, MoreIcon, PlusIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from './DropdownMenu'\n\nconst meta: Meta<typeof DropdownMenu> = {\n  title: 'Components/DropdownMenu',\n  component: DropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const BasicDropdownMenu: Story = {\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n          Show text song\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithIcons: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist')}>\n          <PlusIcon />\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithDisabledItem: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem disabled>Add to playlist (disabled)</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const CustomTrigger: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <IconButton aria-label=\"More options\">\n          <MoreIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Action 1')}>Action 1</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 2')}>Action 2</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 3')}>Action 3</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const DifferentAlignments: Story = {\n  args: {},\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '100px',\n        padding: '100px',\n        alignItems: 'center',\n        backgroundColor: 'var(--color-bg-secondary)',\n      }}>\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align Start\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align Center\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"center\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align End (default)\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </div>\n  ),\n}\n\nexport const WithLinks: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          as=\"a\"\n          href=\"https://example.com\"\n          target=\"_blank\"\n          onClick={() => console.log('Link clicked')}>\n          <PlusIcon />\n          Visit Website\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const Interactive: Story = {\n  args: {},\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '20px',\n        alignItems: 'center',\n        padding: '40px',\n      }}>\n      <Typography variant=\"h3\">Click the menu buttons to test functionality</Typography>\n\n      <div style={{ display: 'flex', gap: '20px' }}>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent>\n            <DropdownMenuItem onClick={() => console.log('Edit clicked')}>\n              <CreateIcon />\n              Edit track\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Add to playlist clicked')}>\n              <PlusIcon />\n              Add to playlist\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem\n              as=\"a\"\n              href=\"https://example.com\"\n              target=\"_blank\"\n              onClick={() => console.log('External link clicked')}>\n              Show lyrics online\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Download clicked')}>\n              Download\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem disabled>Share (coming soon)</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <IconButton aria-label=\"Playlist options\">\n              <MoreIcon />\n            </IconButton>\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem onClick={() => console.log('Edit playlist')}>\n              Edit playlist\n            </DropdownMenuItem>\n            <DropdownMenuItem\n              as=\"a\"\n              href=\"/share/playlist\"\n              onClick={() => console.log('Share playlist')}>\n              Share playlist\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Delete playlist')}>\n              Delete playlist\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <Typography variant=\"caption\">Open browser console to see click events</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/DropdownMenu/DropdownMenu.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  createContext,\n  type ElementType,\n  type ReactNode,\n  use,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\n\nimport s from './DropdownMenu.module.scss'\n\ntype DropdownMenuContextType = {\n  isOpen: boolean\n  onClose: () => void\n  onToggle: () => void\n  triggerRef: React.RefObject<HTMLElement | null>\n}\n\nconst DropdownMenuContext = createContext<DropdownMenuContextType | null>(null)\n\nconst useDropdownMenuContext = () => {\n  const context = use(DropdownMenuContext)\n  if (!context) {\n    throw new Error('DropdownMenu compound components must be used within DropdownMenu component')\n  }\n  return context\n}\n\n/*\n * DropdownMenu\n */\n\nexport type DropdownMenuProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DropdownMenu = ({ children, className }: DropdownMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false)\n  const triggerRef = useRef<HTMLElement>(null)\n\n  const onClose = () => setIsOpen(false)\n  const onToggle = () => setIsOpen(!isOpen)\n\n  useBlockScroll({ isOpen, triggerRef })\n\n  // Close on escape key\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n    return () => document.removeEventListener('keydown', handleKeyDown)\n  }, [isOpen])\n\n  // Close on click outside\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as Element\n      if (\n        triggerRef.current &&\n        !triggerRef.current.contains(target) &&\n        !target.closest('[data-dropdown-content]')\n      ) {\n        onClose()\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen])\n\n  const contextValue = {\n    isOpen,\n    onClose,\n    onToggle,\n    triggerRef,\n  }\n\n  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    e.stopPropagation()\n    e.preventDefault()\n  }\n\n  return (\n    <div className={clsx(s.container, className)} onClick={handleClick}>\n      <DropdownMenuContext value={contextValue}>{children}</DropdownMenuContext>\n    </div>\n  )\n}\n\n/*\n * DropdownMenuTrigger\n */\n\nexport type DropdownMenuTriggerProps = {\n  children: ReactNode\n  className?: string\n  asChild?: boolean\n  onClick?: (e: React.MouseEvent) => void\n}\n\nexport const DropdownMenuTrigger = ({\n  children,\n  className,\n  asChild = false,\n  onClick,\n}: DropdownMenuTriggerProps) => {\n  const { isOpen, onToggle, triggerRef } = useDropdownMenuContext()\n\n  const handleClick = (e: React.MouseEvent) => {\n    onToggle()\n    onClick?.(e)\n  }\n\n  if (asChild) {\n    return (\n      <div\n        ref={triggerRef as React.RefObject<HTMLDivElement>}\n        onClick={handleClick}\n        className={className}\n        data-open={isOpen ? '' : undefined}>\n        {children}\n      </div>\n    )\n  }\n\n  return (\n    <button\n      ref={triggerRef as React.RefObject<HTMLButtonElement>}\n      type=\"button\"\n      onClick={handleClick}\n      data-open={isOpen ? '' : undefined}\n      className={clsx(s.trigger, className)}>\n      {children}\n    </button>\n  )\n}\n\n/*\n * DropdownMenuContent\n */\n\nexport type DropdownMenuContentProps = {\n  children: ReactNode\n  className?: string\n  align?: 'start' | 'center' | 'end'\n  side?: 'top' | 'bottom' | 'left' | 'right'\n}\n\nexport const DropdownMenuContent = ({\n  children,\n  className,\n  align = 'end',\n  side = 'bottom',\n}: DropdownMenuContentProps) => {\n  const { isOpen, triggerRef } = useDropdownMenuContext()\n  const [position, setPosition] = useState({ top: 0, left: 0 })\n\n  // it's needed to prevent flickering\n  const [isPositioned, setIsPositioned] = useState(false)\n\n  useEffect(() => {\n    if (!isOpen || !triggerRef.current) {\n      setIsPositioned(false)\n      return\n    }\n\n    const triggerRect = triggerRef.current.getBoundingClientRect()\n    const scrollTop = window.pageYOffset || document.documentElement.scrollTop\n    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft\n\n    let top = 0\n    let left = 0\n\n    // Calculate position based on side\n    switch (side) {\n      case 'bottom':\n        top = triggerRect.bottom + scrollTop + 4\n        break\n      case 'top':\n        top = triggerRect.top + scrollTop - 4\n        break\n      case 'right':\n        left = triggerRect.right + scrollLeft + 4\n        top = triggerRect.top + scrollTop\n        break\n      case 'left':\n        left = triggerRect.left + scrollLeft - 4\n        top = triggerRect.top + scrollTop\n        break\n    }\n\n    // Calculate position based on align\n    if (side === 'bottom' || side === 'top') {\n      switch (align) {\n        case 'start':\n          left = triggerRect.left + scrollLeft\n          break\n        case 'center':\n          left = triggerRect.left + scrollLeft + triggerRect.width / 2\n          break\n        case 'end':\n          left = triggerRect.right + scrollLeft\n          break\n      }\n    }\n\n    setPosition({ top, left })\n    setIsPositioned(true)\n  }, [isOpen, align, side])\n\n  if (!isOpen || !isPositioned) return null\n\n  const content = (\n    <div\n      className={clsx(s.content, s[`align-${align}`], s[`side-${side}`], className)}\n      style={{ top: position.top, left: position.left }}\n      data-dropdown-content\n      role=\"menu\">\n      {children}\n    </div>\n  )\n\n  return createPortal(content, document.body)\n}\n\n/*\n * DropdownMenuItem\n */\n\nexport type DropdownMenuItemProps<T extends ElementType = 'button'> = {\n  as?: T\n  children: ReactNode\n  onClick?: () => void\n  className?: string\n  disabled?: boolean\n} & ComponentProps<T>\n\nexport const DropdownMenuItem = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  onClick,\n  className,\n  disabled = false,\n  ...props\n}: DropdownMenuItemProps<T>) => {\n  const { onClose } = useDropdownMenuContext()\n\n  const handleClick = () => {\n    if (disabled) return\n    onClick?.()\n    onClose()\n  }\n\n  const isButton = Component === 'button'\n\n  return (\n    <Component\n      {...(isButton && { type: 'button' })}\n      className={clsx(s.item, disabled && s.itemDisabled, className)}\n      onClick={handleClick}\n      {...(isButton && { disabled })}\n      role=\"menuitem\"\n      {...props}>\n      {children}\n    </Component>\n  )\n}\n\n/*\n * DropdownMenuSeparator\n */\n\nexport type DropdownMenuSeparatorProps = {\n  className?: string\n}\n\nexport const DropdownMenuSeparator = ({ className }: DropdownMenuSeparatorProps) => {\n  return <div className={clsx(s.separator, className)} role=\"separator\" />\n}\n\n/**\n * Block scroll when menu is open.\n */\nconst useBlockScroll = ({\n  isOpen,\n  triggerRef,\n}: {\n  isOpen: boolean\n  triggerRef: React.RefObject<HTMLElement | null>\n}) => {\n  // Block scroll when menu is open\n  useEffect(() => {\n    if (!isOpen || !triggerRef.current) return\n\n    const originalScrollElements: Array<{ element: Element; overflow: string }> = []\n\n    // Find all scrollable parent elements\n    const findScrollableParents = (element: Element) => {\n      const scrollableElements: Element[] = []\n      let parent = element.parentElement\n\n      while (parent && parent !== document.body) {\n        const style = window.getComputedStyle(parent)\n        const hasVerticalScroll =\n          style.overflowY === 'auto' ||\n          style.overflowY === 'scroll' ||\n          style.overflow === 'auto' ||\n          style.overflow === 'scroll'\n\n        if (hasVerticalScroll && parent.scrollHeight > parent.clientHeight) {\n          scrollableElements.push(parent)\n        }\n        parent = parent.parentElement\n      }\n\n      return scrollableElements\n    }\n\n    // Block scroll on body\n    const bodyOverflow = document.body.style.overflow\n    document.body.style.overflow = 'hidden'\n    originalScrollElements.push({ element: document.body, overflow: bodyOverflow })\n\n    // Block scroll on scrollable parents\n    const scrollableParents = findScrollableParents(triggerRef.current)\n    scrollableParents.forEach((element) => {\n      const originalOverflow = (element as HTMLElement).style.overflow\n      ;(element as HTMLElement).style.overflow = 'hidden'\n      originalScrollElements.push({ element, overflow: originalOverflow })\n    })\n\n    return () => {\n      // Restore original overflow values\n      originalScrollElements.forEach(({ element, overflow }) => {\n        ;(element as HTMLElement).style.overflow = overflow\n      })\n    }\n  }, [isOpen])\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/DropdownMenu/index.ts",
    "content": "export * from './DropdownMenu'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/FormControlledTextField/FormControlledTextField.tsx",
    "content": "import {\n  type Control,\n  type FieldPath,\n  type FieldValues,\n  type RegisterOptions,\n  useController,\n} from 'react-hook-form'\n\nimport { TextField, type TextFieldProps } from '@/shared/components'\n\ntype FormControlledTextFieldProps<T extends FieldValues> = {\n  name: FieldPath<T>\n  control: Control<T>\n  rules?: RegisterOptions<T, FieldPath<T>>\n} & Omit<TextFieldProps, 'name' | 'value' | 'onChange' | 'onBlur' | 'ref'>\n\nexport const FormControlledTextField = <T extends FieldValues>({\n  name,\n  control,\n  rules,\n  ...rest\n}: FormControlledTextFieldProps<T>) => {\n  const {\n    field,\n    fieldState: { error },\n  } = useController({ name, control, rules })\n\n  return <TextField {...field} {...rest} errorMessage={error?.message} />\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/FormControlledTextField/index.ts",
    "content": "export * from './FormControlledTextField'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Hashtag/Tag.module.css",
    "content": ".hashtag {\n  cursor: pointer;\n\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n\n  min-width: 73px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 45px;\n\n  font-size: var(--font-size-xxxs);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  text-decoration: none;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.hashtag:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.hashtag:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.active {\n  color: var(--color-bg-primary);\n  background-color: var(--color-text-primary);\n}\n\n.active:hover:not(:disabled) {\n  color: var(--color-bg-primary);\n  opacity: 0.9;\n  background-color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Hashtag/Tag.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Tag } from './Tag.tsx'\n\nconst meta = {\n  title: 'Components/Hashtag',\n  component: Tag,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    tag: 'Playlists',\n  },\n} satisfies Meta<typeof Tag>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n\nexport const Active: Story = {\n  args: {\n    active: true,\n  },\n}\n\nexport const AsLink: Story = {\n  args: {\n    as: 'a',\n    href: 'https://www.google.com',\n    target: '_blank',\n  },\n}\n\nexport const AllHashtags: Story = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '16px' }}>\n      <Tag tag=\"Playlists\" />\n      <Tag active tag=\"Artists\" />\n      <Tag tag=\"Albums\" />\n      <Tag as=\"a\" href=\"#\" tag=\"Podcasts & shows\">\n        Podcasts & shows\n      </Tag>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Hashtag/Tag.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Tag.module.css'\n\nexport type HashtagProps<T extends ElementType = 'button'> = {\n  as?: T\n  active?: boolean\n  tag: string\n  className?: string\n} & ComponentProps<T>\n\nexport const Tag = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  active = false,\n  tag,\n  className,\n  ...props\n}: HashtagProps<T>) => {\n  const classNames = clsx(s.hashtag, active && s.active, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      #{tag}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Hashtag/index.ts",
    "content": "export * from './Tag.tsx'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/IconButton/IconButton.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.button:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.button:enabled:hover,\n.button:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/IconButton/IconButton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport {\n  DownloadIcon,\n  HomeIcon,\n  LikeIcon,\n  MoreIcon,\n  PlayIcon,\n  PlusIcon,\n  SearchIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from './IconButton'\n\nconst meta = {\n  title: 'Components/IconButton',\n  component: IconButton,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof IconButton>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    children: <PlayIcon />,\n    'aria-label': 'Play',\n  },\n}\n\nexport const AllIcons = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '16px',\n        flexWrap: 'wrap',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: '20px',\n      }}>\n      <IconButton aria-label=\"Home\">\n        <HomeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Search\">\n        <SearchIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Play\">\n        <PlayIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Like\">\n        <LikeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Add\">\n        <PlusIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"More options\">\n        <MoreIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Download\">\n        <DownloadIcon />\n      </IconButton>\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    children: <PlayIcon />,\n    disabled: true,\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/IconButton/IconButton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './IconButton.module.css'\n\ntype IconButtonProps = {\n  children: React.ReactNode\n} & ComponentProps<'button'>\n\nexport const IconButton = ({ children, className, ...props }: IconButtonProps) => {\n  return (\n    <button type=\"button\" className={clsx(s.button, className)} {...props}>\n      {children}\n    </button>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/IconButton/index.ts",
    "content": "export * from './IconButton'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ImageCropper/ImageCropper.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 600px;\n}\n\n.cropperContainer {\n  position: relative;\n\n  overflow: hidden;\n\n  width: 100%;\n  height: 300px;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n}\n\n.previewImage {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.round {\n  border-radius: 50%;\n}\n\n.zoomControls {\n  margin-top: 16px;\n  padding: 16px;\n  border-radius: 4px;\n  background-color: var(--color-bg-secondary);\n}\n\n.zoomLabel {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 8px;\n}\n\n.zoomValue {\n  font-weight: 600;\n  color: var(--color-accent);\n}\n\n.zoomSlider {\n  cursor: pointer;\n\n  width: 100%;\n  height: 4px;\n  border-radius: 2px;\n\n  appearance: none;\n  background-color: var(--color-border-base);\n  outline: none;\n\n  transition: opacity 200ms ease;\n}\n\n.zoomSlider:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.zoomSlider::-webkit-slider-thumb {\n  cursor: pointer;\n\n  width: 16px;\n  height: 16px;\n  border-radius: 50%;\n\n  appearance: none;\n  background-color: var(--color-accent);\n\n  transition: all 200ms ease;\n}\n\n.zoomSlider::-webkit-slider-thumb:hover:not(:disabled) {\n  transform: scale(1.1);\n}\n\n.zoomSlider::-moz-range-thumb {\n  cursor: pointer;\n\n  width: 16px;\n  height: 16px;\n  border: none;\n  border-radius: 50%;\n\n  background-color: var(--color-accent);\n\n  transition: all 200ms ease;\n}\n\n.zoomSlider::-moz-range-thumb:hover:not(:disabled) {\n  transform: scale(1.1);\n}\n\n.zoomSlider::-moz-range-track {\n  width: 100%;\n  height: 4px;\n  border-radius: 2px;\n  background-color: var(--color-border-base);\n}\n\n.zoomSlider:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ImageCropper/ImageCropper.tsx",
    "content": "import { clsx } from 'clsx'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Dialog, DialogContent, DialogFooter } from '../Dialog'\nimport { Typography } from '../Typography'\nimport s from './ImageCropper.module.css'\n\nexport type CropShape = 'rect' | 'round'\nexport type Area = {\n  width: number\n  height: number\n  x: number\n  y: number\n}\n\nexport type ImageCropperProps = {\n  isOpen: boolean\n  onClose: () => void\n  onCropComplete: (croppedFile: File, croppedArea: Area) => void\n  imageSrc: string\n  originalFileName?: string\n  cropShape?: CropShape\n  className?: string\n}\n\nexport const ImageCropper = ({\n  isOpen,\n  onClose,\n  onCropComplete,\n  imageSrc,\n  originalFileName = 'cropped-image.jpg',\n  cropShape = 'rect',\n  className,\n}: ImageCropperProps) => {\n  const [focusX, setFocusX] = useState(50)\n  const [focusY, setFocusY] = useState(50)\n  const [zoom, setZoom] = useState(1)\n  const [isProcessing, setIsProcessing] = useState(false)\n  const cropSize = 300\n\n  const getCroppedImg = async (source: string): Promise<{ file: File; area: Area }> => {\n    const image = new Image()\n    image.src = source\n\n    return new Promise((resolve, reject) => {\n      image.onload = () => {\n        const canvas = document.createElement('canvas')\n        const ctx = canvas.getContext('2d')\n\n        if (!ctx) {\n          reject(new Error('No 2d context'))\n          return\n        }\n\n        canvas.width = cropSize\n        canvas.height = cropSize\n\n        const baseScale = Math.max(cropSize / image.naturalWidth, cropSize / image.naturalHeight)\n        const scaledWidth = image.naturalWidth * baseScale * zoom\n        const scaledHeight = image.naturalHeight * baseScale * zoom\n\n        const focusXPx = (focusX / 100) * scaledWidth\n        const focusYPx = (focusY / 100) * scaledHeight\n\n        const maxLeft = Math.max(0, scaledWidth - cropSize)\n        const maxTop = Math.max(0, scaledHeight - cropSize)\n\n        const windowLeft = Math.min(Math.max(focusXPx - cropSize / 2, 0), maxLeft)\n        const windowTop = Math.min(Math.max(focusYPx - cropSize / 2, 0), maxTop)\n\n        const sourceX = windowLeft / (baseScale * zoom)\n        const sourceY = windowTop / (baseScale * zoom)\n        const sourceWidth = cropSize / (baseScale * zoom)\n        const sourceHeight = cropSize / (baseScale * zoom)\n\n        ctx.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, cropSize, cropSize)\n\n        canvas.toBlob(\n          (blob) => {\n            if (!blob) {\n              reject(new Error('Canvas is empty'))\n              return\n            }\n\n            const file = new File([blob], originalFileName, { type: 'image/jpeg' })\n            resolve({\n              file,\n              area: {\n                x: Math.round(sourceX),\n                y: Math.round(sourceY),\n                width: Math.round(sourceWidth),\n                height: Math.round(sourceHeight),\n              },\n            })\n          },\n          'image/jpeg',\n          0.9\n        )\n      }\n\n      image.onerror = () => {\n        reject(new Error('Failed to load image'))\n      }\n    })\n  }\n\n  const handleCropConfirm = async () => {\n    setIsProcessing(true)\n    try {\n      const { file, area } = await getCroppedImg(imageSrc)\n      onCropComplete(file, area)\n      handleReset()\n    } catch (error) {\n      console.error('Error cropping image:', error)\n    } finally {\n      setIsProcessing(false)\n    }\n  }\n\n  const handleReset = () => {\n    setFocusX(50)\n    setFocusY(50)\n    setZoom(1)\n    setIsProcessing(false)\n  }\n\n  const handleCancel = () => {\n    handleReset()\n    onClose()\n  }\n\n  const handleClose = () => {\n    if (!isProcessing) {\n      handleCancel()\n    }\n  }\n\n  return (\n    <Dialog open={isOpen} onClose={handleClose} className={clsx(s.dialog, className)}>\n      <DialogContent className={s.dialogContent}>\n        <div className={s.cropperContainer}>\n          {imageSrc && (\n            <img\n              src={imageSrc}\n              alt=\"Crop preview\"\n              className={clsx(s.previewImage, cropShape === 'round' && s.round)}\n              style={{\n                transform: `scale(${zoom})`,\n                transformOrigin: `${focusX}% ${focusY}%`,\n              }}\n            />\n          )}\n        </div>\n\n        <div className={s.zoomControls}>\n          <div className={s.zoomLabel}>\n            <Typography variant=\"body2\">Focus X</Typography>\n            <Typography variant=\"body2\" className={s.zoomValue}>\n              {Math.round(focusX)}%\n            </Typography>\n          </div>\n          <input\n            type=\"range\"\n            value={focusX}\n            min={0}\n            max={100}\n            step={1}\n            onChange={(e) => setFocusX(Number(e.target.value))}\n            className={s.zoomSlider}\n            disabled={isProcessing}\n          />\n        </div>\n\n        <div className={s.zoomControls}>\n          <div className={s.zoomLabel}>\n            <Typography variant=\"body2\">Focus Y</Typography>\n            <Typography variant=\"body2\" className={s.zoomValue}>\n              {Math.round(focusY)}%\n            </Typography>\n          </div>\n          <input\n            type=\"range\"\n            value={focusY}\n            min={0}\n            max={100}\n            step={1}\n            onChange={(e) => setFocusY(Number(e.target.value))}\n            className={s.zoomSlider}\n            disabled={isProcessing}\n          />\n        </div>\n\n        <div className={s.zoomControls}>\n          <div className={s.zoomLabel}>\n            <Typography variant=\"body2\">Zoom</Typography>\n            <Typography variant=\"body2\" className={s.zoomValue}>\n              {Math.round(zoom * 100)}%\n            </Typography>\n          </div>\n          <input\n            type=\"range\"\n            value={zoom}\n            min={1}\n            max={3}\n            step={0.05}\n            onChange={(e) => setZoom(Number(e.target.value))}\n            className={s.zoomSlider}\n            disabled={isProcessing}\n          />\n        </div>\n      </DialogContent>\n\n      <DialogFooter>\n        <Button variant=\"secondary\" onClick={handleCancel} disabled={isProcessing}>\n          Cancel\n        </Button>\n        <Button variant=\"primary\" onClick={handleCropConfirm} disabled={isProcessing}>\n          {isProcessing ? 'Processing...' : 'Apply Crop'}\n        </Button>\n      </DialogFooter>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ImageCropper/index.ts",
    "content": "export * from './ImageCropper'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ImageUploader/ImageUploader.module.css",
    "content": ".container {\n  width: 100%;\n}\n\n.dropZone {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 100%;\n  min-height: 280px;\n  border: 2px dashed var(--color-border-input-primary);\n  border-radius: 8px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover,\n.dropZone:focus-within {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.dragOver {\n  border-color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.hasPreview {\n  border-color: var(--color-border-input-active);\n  border-style: solid;\n}\n\n.dropZone.error {\n  border-color: var(--color-text-error);\n}\n\n.hiddenInput {\n  position: absolute;\n\n  overflow: hidden;\n\n  width: 1px;\n  height: 1px;\n\n  opacity: 0;\n  clip: rect(0, 0, 0, 0);\n}\n\n.uploadContent {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  align-items: center;\n\n  padding: 32px 16px;\n}\n\n.uploadIcon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n\n  color: var(--color-text-secondary);\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover .uploadIcon,\n.dropZone:focus-within .uploadIcon {\n  color: var(--color-accent);\n  background-color: var(--color-bg-card);\n}\n\n.uploadText {\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  transition: color 200ms ease;\n}\n\n.dropZone:hover .uploadText {\n  color: var(--color-text-primary);\n}\n\n.previewContainer {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.previewImage {\n  width: 100%;\n  height: 100%;\n  min-height: 200px;\n  border-radius: 6px;\n\n  object-fit: cover;\n}\n\n.removeButton {\n  position: absolute;\n  top: 8px;\n  right: 8px;\n}\n\n.removeButton:hover {\n  opacity: 1;\n  background-color: var(--color-text-error);\n}\n\n.removeButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.errorMessage {\n  margin-top: 8px;\n}\n\n/* States for different sizes */\n.dropZone.small {\n  min-height: 120px;\n}\n\n.dropZone.large {\n  min-height: 300px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ImageUploader/ImageUploader.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ImageUploader } from './ImageUploader'\n\nconst meta = {\n  title: 'Components/ImageUploader',\n  component: ImageUploader,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    onImageSelect: () => {},\n  },\n} satisfies Meta<typeof ImageUploader>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    placeholder: 'Upload Cover Image',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const CustomPlaceholder: Story = {\n  args: {\n    placeholder: 'Choose your avatar',\n  },\n  render: (args) => (\n    <div style={{ width: '200px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const WithCustomLimits: Story = {\n  args: {\n    placeholder: 'Upload image (max 2MB)',\n    maxSizeInMB: 2,\n    acceptedFormats: ['image/jpeg', 'image/png'],\n  },\n  render: (args) => (\n    <div style={{ width: '400px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const AllowAllImages: Story = {\n  args: {\n    placeholder: 'Upload any image format',\n    acceptedFormats: ['image/*'],\n    maxSizeInMB: 10,\n  },\n  render: (args) => (\n    <div style={{ width: '350px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const Interactive: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '400px',\n      }}>\n      <div>\n        <h3 style={{ color: 'var(--color-text-primary)', marginBottom: '12px' }}>Profile Avatar</h3>\n        <ImageUploader\n          placeholder=\"Upload avatar\"\n          onImageSelect={(file) => console.log('Avatar selected:', file.name)}\n          maxSizeInMB={1}\n        />\n      </div>\n\n      <div>\n        <h3 style={{ color: 'var(--color-text-primary)', marginBottom: '12px' }}>Playlist Cover</h3>\n        <ImageUploader\n          placeholder=\"Upload Cover Image\"\n          onImageSelect={(file) => console.log('Cover selected:', file.name)}\n          maxSizeInMB={5}\n        />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ImageUploader/ImageUploader.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ChangeEvent, type DragEvent, useEffect, useRef, useState } from 'react'\n\nimport { ImageUploadIcon } from '@/shared/icons'\nimport { t } from 'i18next'\n\nimport { IconButton } from '../IconButton'\nimport { type CropShape, ImageCropper } from '../ImageCropper'\nimport { Typography } from '../Typography'\nimport s from './ImageUploader.module.css'\n\nexport type ImageUploaderProps = {\n  onImageSelect: (file: File) => void\n  className?: string\n  acceptedFormats?: string[]\n  maxSizeInMB?: number\n  placeholder?: string\n  cropShape?: CropShape\n  aspectRatio?: number\n  enableCrop?: boolean\n  cropTitle?: string\n  cropDescription?: string\n  initialImageUrl?: string\n}\n\nconst ACCEPTED_FORMATS = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']\n\nexport const ImageUploader = ({\n  className,\n  onImageSelect,\n  acceptedFormats = ACCEPTED_FORMATS,\n  maxSizeInMB = 5,\n  placeholder = t('placeholder.upload_cover_image'),\n  cropShape = 'rect',\n  enableCrop = true,\n  initialImageUrl,\n}: ImageUploaderProps) => {\n  const [isDragOver, setIsDragOver] = useState(false)\n  const [preview, setPreview] = useState<string | null>(initialImageUrl || null)\n  const [originalFile, setOriginalFile] = useState<File | null>(null)\n  const [error, setError] = useState<string | null>(null)\n  const [showCropModal, setShowCropModal] = useState(false)\n  const fileInputRef = useRef<HTMLInputElement>(null)\n\n  useEffect(() => {\n    setPreview(initialImageUrl || null)\n  }, [initialImageUrl])\n\n  const validateFile = (file: File): string | null => {\n    if (!acceptedFormats.includes(file.type)) {\n      return `Only ${acceptedFormats.join(', ')} files are allowed`\n    }\n\n    const maxSizeInBytes = maxSizeInMB * 1024 * 1024\n    if (file.size > maxSizeInBytes) {\n      return `File size must be less than ${maxSizeInMB}MB`\n    }\n\n    return null\n  }\n\n  const handleFileSelect = (file: File) => {\n    const validationError = validateFile(file)\n\n    if (validationError) {\n      setError(validationError)\n      setPreview(null)\n      return\n    }\n\n    setError(null)\n    setOriginalFile(file)\n\n    const reader = new FileReader()\n    reader.onload = (e) => {\n      const imageUrl = e.target?.result as string\n      setPreview(imageUrl)\n\n      if (enableCrop) {\n        setShowCropModal(true)\n      } else {\n        onImageSelect(file)\n      }\n    }\n    reader.readAsDataURL(file)\n  }\n\n  const handleCropComplete = (croppedFile: File) => {\n    const reader = new FileReader()\n    reader.onload = (e) => {\n      setPreview(e.target?.result as string)\n    }\n    reader.readAsDataURL(croppedFile)\n\n    setShowCropModal(false)\n    onImageSelect(croppedFile)\n  }\n\n  const handleCropCancel = () => {\n    setShowCropModal(false)\n    setPreview(initialImageUrl || null)\n    setOriginalFile(null)\n  }\n\n  const handleFileInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0]\n    if (file) {\n      handleFileSelect(file)\n    }\n  }\n\n  const handleDragOver = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(true)\n  }\n\n  const handleDragLeave = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n  }\n\n  const handleDrop = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n\n    const files = Array.from(e.dataTransfer.files)\n    const imageFile = files.find((file) => file.type.startsWith('image/'))\n\n    if (imageFile) {\n      handleFileSelect(imageFile)\n    }\n  }\n\n  const handleRemoveImage = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    setPreview(null)\n    setOriginalFile(null)\n    setError(null)\n    if (fileInputRef.current) {\n      fileInputRef.current.value = ''\n    }\n  }\n\n  return (\n    <>\n      <div className={clsx(s.container, className)}>\n        <label\n          className={clsx(\n            s.dropZone,\n            isDragOver && s.dragOver,\n            preview && s.hasPreview,\n            error && s.error\n          )}\n          onDragOver={handleDragOver}\n          onDragLeave={handleDragLeave}\n          onDrop={handleDrop}>\n          <input\n            ref={fileInputRef}\n            type=\"file\"\n            accept={acceptedFormats.join(',')}\n            onChange={handleFileInputChange}\n            className={s.hiddenInput}\n            tabIndex={0}\n          />\n\n          {preview ? (\n            <div className={s.previewContainer}>\n              <img src={preview} alt=\"Preview\" className={s.previewImage} />\n              <IconButton\n                className={s.removeButton}\n                onClick={handleRemoveImage}\n                aria-label=\"Remove image\"\n                type=\"button\">\n                ✕\n              </IconButton>\n            </div>\n          ) : (\n            <div className={s.uploadContent}>\n              <div className={s.uploadIcon}>\n                <ImageUploadIcon width={24} height={24} />\n              </div>\n              <Typography variant=\"body2\" className={s.uploadText}>\n                {placeholder}\n              </Typography>\n            </div>\n          )}\n        </label>\n\n        {error && (\n          <Typography variant=\"error\" className={s.errorMessage}>\n            {error}\n          </Typography>\n        )}\n      </div>\n\n      {enableCrop && preview && originalFile && (\n        <ImageCropper\n          isOpen={showCropModal}\n          onClose={handleCropCancel}\n          onCropComplete={handleCropComplete}\n          imageSrc={preview}\n          originalFileName={originalFile.name}\n          cropShape={cropShape}\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ImageUploader/index.ts",
    "content": "export * from './ImageUploader'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/LanguageSwitcher/LanguageSwitcher.tsx",
    "content": "import {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/shared/components'\nimport { LanguageIcon } from '@/shared/icons/LanguageIcon'\nimport { setLocale } from '@/shared/utils/set-locale'\n\nexport const LanguageSwitcher = () => {\n  const languageDisplayNames: Record<string, string> = {\n    en: 'English',\n    ru: 'Русский',\n  }\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <LanguageIcon />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuItem\n          onClick={() => {\n            setLocale('en')\n          }}>\n          {languageDisplayNames['en']}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          onClick={() => {\n            setLocale('ru')\n          }}>\n          {languageDisplayNames['ru']}\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/LanguageSwitcher/index.ts",
    "content": "export { LanguageSwitcher } from './LanguageSwitcher'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Pagination/Pagination.module.css",
    "content": ".pagination {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n}\n\n.navButton {\n  width: 40px;\n  height: 40px;\n  border-radius: 4px;\n\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.navButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n  background-color: var(--color-bg-secondary);\n}\n\n.navButton:enabled:hover,\n.navButton:enabled:focus {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageNumbers {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n}\n\n.pageButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n  border: none;\n  border-radius: 8px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.pageButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.pageButton:hover:not(.active) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageButton.active {\n  background-color: var(--color-accent);\n}\n\n.pageButton.active:hover {\n  opacity: 0.9;\n}\n\n.ellipsis {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* Responsive adjustments */\n@media (width <= 480px) {\n  .pagination {\n    gap: 2px;\n  }\n\n  .navButton,\n  .pageButton,\n  .ellipsis {\n    width: 36px;\n    height: 36px;\n  }\n\n  .pageButton,\n  .ellipsis {\n    font-size: var(--font-size-s);\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Pagination/Pagination.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Pagination } from './Pagination'\n\nconst meta = {\n  title: 'Components/Pagination',\n  component: Pagination,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Pagination>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    page: 1,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const MiddlePage: Story = {\n  args: {\n    page: 5,\n    pagesCount: 10,\n    onPageChange: () => {},\n  },\n}\n\nexport const LastPage: Story = {\n  args: {\n    page: 3,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const ManyPages: Story = {\n  args: {\n    page: 8,\n    pagesCount: 20,\n    onPageChange: () => {},\n  },\n}\n\nexport const SinglePage: Story = {\n  args: {\n    page: 1,\n    pagesCount: 1,\n    onPageChange: () => {},\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [currentPage, setCurrentPage] = useState(1)\n    const totalCount = 95\n    const pageSize = 10\n    const pagesCount = Math.ceil(totalCount / pageSize)\n\n    const handlePageChange = (page: number) => {\n      setCurrentPage(page)\n    }\n\n    return (\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n          alignItems: 'center',\n          width: '500px',\n        }}>\n        <Card style={{ padding: '20px', textAlign: 'center' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Interactive Pagination\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Current page: <strong>{currentPage}</strong>\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Total items: <strong>{totalCount}</strong>\n          </Typography>\n          <Typography variant=\"body2\">\n            Items per page: <strong>{pageSize}</strong>\n          </Typography>\n        </Card>\n\n        <Pagination page={currentPage} pagesCount={pagesCount} onPageChange={handlePageChange} />\n\n        <Typography variant=\"caption\" style={{ textAlign: 'center' }}>\n          Click on page numbers or arrows to navigate\n        </Typography>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '32px',\n        alignItems: 'center',\n        width: '600px',\n      }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          First Page (3 pages total)\n        </Typography>\n        <Pagination page={1} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Middle Page (10 pages total)\n        </Typography>\n        <Pagination page={5} pagesCount={10} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Last Page (3 pages total)\n        </Typography>\n        <Pagination page={3} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Many Pages (20 pages total)\n        </Typography>\n        <Pagination page={12} pagesCount={20} onPageChange={() => {}} />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Pagination/Pagination.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport { KeyboardArrowLeftIcon, KeyboardArrowRightIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './Pagination.module.css'\n\nexport type PaginationProps = {\n  page: number\n  pagesCount: number\n  onPageChange: (page: number) => void\n  alwaysVisible?: boolean\n  className?: string\n} & Omit<ComponentProps<'div'>, 'children'>\n\nconst MAX_VISIBLE_PAGES = 5\n\nexport const Pagination = ({\n  page,\n\n  pagesCount,\n  onPageChange,\n  alwaysVisible = false,\n  className,\n  ...props\n}: PaginationProps) => {\n  const normalizedPagesCount = Math.max(1, pagesCount)\n  const normalizedPage = Math.min(Math.max(1, page), normalizedPagesCount)\n\n  if (!alwaysVisible && normalizedPagesCount <= 1) {\n    return null\n  }\n\n  // Helper function to generate page numbers array\n  const generatePageNumbers = () => {\n    const pages: (number | 'ellipsis')[] = []\n\n    if (normalizedPagesCount <= MAX_VISIBLE_PAGES) {\n      // Show all pages if total is small\n      for (let i = 1; i <= normalizedPagesCount; i++) {\n        pages.push(i)\n      }\n    } else {\n      // Always show first page\n      pages.push(1)\n\n      if (normalizedPage > 3) {\n        pages.push('ellipsis')\n      }\n\n      // Show pages around current page\n      const start = Math.max(2, normalizedPage - 1)\n      const end = Math.min(normalizedPagesCount - 1, normalizedPage + 1)\n\n      for (let i = start; i <= end; i++) {\n        if (i !== 1 && i !== normalizedPagesCount) {\n          pages.push(i)\n        }\n      }\n\n      if (normalizedPage < normalizedPagesCount - 2) {\n        pages.push('ellipsis')\n      }\n\n      // Always show last page if it's not already included\n      if (normalizedPagesCount > 1) {\n        pages.push(normalizedPagesCount)\n      }\n    }\n\n    return pages\n  }\n\n  const handlePrevious = () => {\n    if (normalizedPage > 1) {\n      onPageChange(normalizedPage - 1)\n    }\n  }\n\n  const handleNext = () => {\n    if (normalizedPage < normalizedPagesCount) {\n      onPageChange(normalizedPage + 1)\n    }\n  }\n\n  const handlePageClick = (pageNumber: number) => {\n    onPageChange(pageNumber)\n  }\n\n  const pageNumbers = generatePageNumbers()\n\n  return (\n    <div\n      className={clsx(s.pagination, className)}\n      role=\"navigation\"\n      aria-label=\"Pagination\"\n      {...props}>\n      {/* Previous button */}\n      <IconButton\n        onClick={handlePrevious}\n        disabled={normalizedPage === 1}\n        aria-label=\"Go to previous page\"\n        className={s.navButton}>\n        <KeyboardArrowLeftIcon />\n      </IconButton>\n\n      {/* Page numbers */}\n      <div className={s.pageNumbers}>\n        {pageNumbers.map((pageNumber, index) => {\n          if (pageNumber === 'ellipsis') {\n            return (\n              <span key={`ellipsis-${index}`} className={s.ellipsis} aria-hidden=\"true\">\n                ...\n              </span>\n            )\n          }\n\n          const isActive = pageNumber === normalizedPage\n\n          return (\n            <button\n              key={pageNumber}\n              onClick={() => handlePageClick(pageNumber)}\n              className={clsx(s.pageButton, isActive && s.active)}\n              aria-label={`Go to page ${pageNumber}`}\n              aria-current={isActive ? 'page' : undefined}\n              type=\"button\">\n              {pageNumber}\n            </button>\n          )\n        })}\n      </div>\n\n      {/* Next button */}\n      <IconButton\n        onClick={handleNext}\n        disabled={normalizedPage === normalizedPagesCount}\n        aria-label=\"Go to next page\"\n        className={s.navButton}>\n        <KeyboardArrowRightIcon />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Pagination/index.ts",
    "content": "export * from './Pagination'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Progress/Progress.module.css",
    "content": ".progress {\n  overflow: hidden;\n\n  width: 100%;\n  height: 4px;\n  border-radius: 4px;\n\n  background-color: var(--color-border-base);\n}\n\n.progressBar {\n  height: 100%;\n  border-radius: 4px;\n  background-color: var(--color-accent);\n  transition: width 300ms ease;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Progress/Progress.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Progress } from './Progress'\n\nconst meta = {\n  title: 'Components/Progress',\n  component: Progress,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Progress>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    value: 75,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const CustomMax: Story = {\n  args: {\n    value: 15,\n    max: 20,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Empty: Story = {\n  args: {\n    value: 0,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Full: Story = {\n  args: {\n    value: 100,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Empty (0%)\n        </Typography>\n        <Progress value={0} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Low (25%)\n        </Typography>\n        <Progress value={25} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Medium (50%)\n        </Typography>\n        <Progress value={50} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          High (85%)\n        </Typography>\n        <Progress value={85} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Complete (100%)\n        </Typography>\n        <Progress value={100} />\n      </div>\n    </div>\n  ),\n}\n\nexport const CustomSizes = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Small (height: 4px)\n        </Typography>\n        <Progress value={70} style={{ height: '4px' }} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Default (height: 8px)\n        </Typography>\n        <Progress value={70} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Large (height: 12px)\n        </Typography>\n        <Progress value={70} style={{ height: '12px' }} />\n      </div>\n    </div>\n  ),\n}\n\nexport const Interactive = {\n  render: () => {\n    const [progress, setProgress] = useState(0)\n\n    const handleIncrease = () => {\n      setProgress((prev) => Math.min(prev + 10, 100))\n    }\n\n    const handleDecrease = () => {\n      setProgress((prev) => Math.max(prev - 10, 0))\n    }\n\n    const handleReset = () => {\n      setProgress(0)\n    }\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Card style={{ padding: '24px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Interactive Progress\n          </Typography>\n\n          <div style={{ marginBottom: '16px' }}>\n            <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n              Current progress: {progress}%\n            </Typography>\n            <Progress value={progress} />\n          </div>\n\n          <div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>\n            <Button variant=\"secondary\" onClick={handleDecrease} disabled={progress === 0}>\n              -10%\n            </Button>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset\n            </Button>\n            <Button variant=\"primary\" onClick={handleIncrease} disabled={progress === 100}>\n              +10%\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const FileUploadExample = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Card style={{ padding: '24px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          File Upload Progress\n        </Typography>\n\n        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">image.jpg</Typography>\n              <Typography variant=\"body2\">75%</Typography>\n            </div>\n            <Progress value={75} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">document.pdf</Typography>\n              <Typography variant=\"body2\">100%</Typography>\n            </div>\n            <Progress value={100} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">video.mp4</Typography>\n              <Typography variant=\"body2\">32%</Typography>\n            </div>\n            <Progress value={32} />\n          </div>\n        </div>\n      </Card>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Progress/Progress.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './Progress.module.css'\n\nexport type ProgressProps = {\n  value: number\n  max?: number\n} & ComponentProps<'div'>\n\nexport const Progress = ({ value, max = 100, className, ...props }: ProgressProps) => {\n  const percentage = Math.min(Math.max((value / max) * 100, 0), 100)\n\n  return (\n    <div\n      className={clsx(s.progress, className)}\n      role=\"progressbar\"\n      aria-valuenow={value}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      {...props}>\n      <div className={s.progressBar} style={{ width: `${percentage}%` }} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Progress/index.ts",
    "content": "export * from './Progress'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ReactionButtons/ReactionButtons.module.css",
    "content": ".container {\n  display: flex;\n  gap: 8px;\n  align-items: start;\n}\n\n.button {\n  width: 28px;\n  height: 28px;\n  padding: 0;\n  transition: color 200ms ease;\n}\n\n.button.large {\n  width: 40px;\n  height: 40px;\n}\n\n.button.liked {\n  color: var(--color-accent);\n}\n\n.button.disliked {\n  color: var(--color-accent);\n}\n\n.button:enabled:hover:is(.liked, .disliked),\n.button:enabled:focus:is(.liked, .disliked) {\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.likesCountBox {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.likesCount {\n  font-size: 10px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ReactionButtons/ReactionButtons.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { type CurrentUserReaction, ReactionButtons } from './ReactionButtons'\n\nconst meta = {\n  title: 'Components/ReactionButtons',\n  component: ReactionButtons,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    entityId: '123', // Required prop, providing a default value\n  },\n} satisfies Meta<typeof ReactionButtons>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    currentReaction: 0,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n    onRemoveReaction: () => console.log('Reaction removed!'),\n  },\n}\n\nexport const WithLikesCount: Story = {\n  args: {\n    currentReaction: 0,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n    onRemoveReaction: () => console.log('Reaction removed!'),\n    likesCount: 10,\n  },\n}\n\nexport const LikedState: Story = {\n  args: {\n    currentReaction: 1,\n    onLike: () => console.log('Unlike'),\n    onDislike: () => console.log('Disliked!'),\n    onRemoveReaction: () => console.log('Reaction removed!'),\n  },\n}\n\nexport const DislikedState: Story = {\n  args: {\n    currentReaction: -1,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Remove dislike'),\n    onRemoveReaction: () => console.log('Reaction removed!'),\n  },\n}\n\nexport const LargeSize: Story = {\n  args: {\n    currentReaction: 0,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n    onRemoveReaction: () => console.log('Reaction removed!'),\n    size: 'large',\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [currentReaction, setCurrentReaction] = useState<CurrentUserReaction>(0)\n\n    const handleLike = () => {\n      setCurrentReaction(currentReaction === 1 ? 0 : 1)\n    }\n\n    const handleDislike = () => {\n      setCurrentReaction(currentReaction === -1 ? 0 : -1)\n    }\n\n    const handleRemove = () => {\n      setCurrentReaction(0)\n    }\n\n    return (\n      <Card\n        style={{\n          padding: '24px',\n          maxWidth: '300px',\n        }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          Interactive Reaction Buttons\n        </Typography>\n\n        <Typography variant=\"body2\" style={{ marginBottom: '16px' }}>\n          Try clicking the buttons below:\n        </Typography>\n\n        <div\n          style={{\n            display: 'flex',\n            justifyContent: 'center',\n          }}>\n          <ReactionButtons\n            entityId=\"123\"\n            currentReaction={currentReaction}\n            onLike={handleLike}\n            onDislike={handleDislike}\n            onRemoveReaction={handleRemove}\n          />\n        </div>\n\n        <Typography\n          variant=\"caption\"\n          style={{\n            marginTop: '16px',\n            textAlign: 'center',\n            display: 'block',\n          }}>\n          Status:{' '}\n          {currentReaction === 1\n            ? '👍 Liked'\n            : currentReaction === -1\n              ? '👎 Disliked'\n              : '😐 Neutral'}\n        </Typography>\n      </Card>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '24px',\n        alignItems: 'center',\n      }}>\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Default\n        </Typography>\n        <ReactionButtons\n          entityId=\"123\"\n          currentReaction={0}\n          onLike={() => {}}\n          onDislike={() => {}}\n          onRemoveReaction={() => {}}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Liked\n        </Typography>\n        <ReactionButtons\n          entityId=\"123\"\n          currentReaction={1}\n          onLike={() => {}}\n          onDislike={() => {}}\n          onRemoveReaction={() => {}}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Disliked\n        </Typography>\n        <ReactionButtons\n          entityId=\"123\"\n          currentReaction={-1}\n          onLike={() => {}}\n          onDislike={() => {}}\n          onRemoveReaction={() => {}}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          With likes count\n        </Typography>\n        <ReactionButtons\n          entityId=\"123\"\n          currentReaction={0}\n          onLike={() => {}}\n          onDislike={() => {}}\n          onRemoveReaction={() => {}}\n          likesCount={10}\n        />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ReactionButtons/ReactionButtons.tsx",
    "content": "import { clsx } from 'clsx'\nimport * as React from 'react'\n\nimport { ReactionValue } from '@/shared/api/schema.ts'\nimport { DislikeIcon, LikeIcon, LikeIconFill } from '@/shared/icons'\nimport { VU } from '@/shared/utils'\n\nimport { IconButton } from '../IconButton'\nimport s from './ReactionButtons.module.css'\n\n// duplication of the CurrentUserReaction type to decouple the shared layer from the features layer\nexport type CurrentUserReaction = ReactionValue\n\nexport interface ReactionButtonsProps {\n  entityId: string\n  currentReaction?: CurrentUserReaction\n  onLike: (entityId: string) => void\n  onDislike: (entityId: string) => void\n  onRemoveReaction: (entityId: string) => void\n  likesCount?: number\n  className?: string\n  size?: keyof typeof SIZE_MAP\n}\n\nconst SIZE_MAP = {\n  small: 28,\n  large: 40,\n}\n\nexport const ReactionButtons: React.FC<ReactionButtonsProps> = (props) => {\n  const {\n    entityId,\n    currentReaction = ReactionValue.Value0,\n    onLike,\n    onDislike,\n    onRemoveReaction,\n    likesCount,\n    className,\n    size = 'small',\n  } = props\n\n  const isLiked = currentReaction === 1\n  const isDisliked = currentReaction === -1\n  const iconSize = SIZE_MAP[size]\n\n  const setReaction = React.useCallback(\n    (reaction: CurrentUserReaction) => {\n      if (VU.isValid(entityId)) {\n        switch (true) {\n          case reaction === currentReaction:\n            return onRemoveReaction?.(entityId)\n          case reaction === ReactionValue.Value1:\n            return onLike?.(entityId)\n          case reaction === ReactionValue.ValueMinus1:\n            return onDislike?.(entityId)\n          default:\n            return\n        }\n      }\n    },\n    [entityId, currentReaction, onRemoveReaction, onLike, onDislike]\n  )\n\n  if (!VU.isValidString(entityId)) {\n    return null\n  }\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <div className={s.likesCountBox}>\n        <IconButton\n          onClick={(e) => {\n            e.preventDefault()\n\n            setReaction(ReactionValue.Value1)\n          }}\n          className={clsx(s.button, isLiked && s.liked, size === 'large' && s.large)}\n          aria-label={isLiked ? 'Remove like' : 'Like'}\n          type=\"button\">\n          {isLiked ? (\n            <LikeIconFill width={iconSize} height={iconSize} />\n          ) : (\n            <LikeIcon width={iconSize} height={iconSize} />\n          )}\n        </IconButton>\n        <span className={s.likesCount}>{likesCount}</span>\n      </div>\n      <IconButton\n        onClick={(e) => {\n          e.preventDefault()\n\n          setReaction(ReactionValue.ValueMinus1)\n        }}\n        className={clsx(s.button, isDisliked && s.disliked, size === 'large' && s.large)}\n        aria-label={isDisliked ? 'Remove dislike' : 'Dislike'}\n        type=\"button\">\n        <DislikeIcon width={iconSize} height={iconSize} />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/ReactionButtons/index.ts",
    "content": "export * from './ReactionButtons'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/SearchField/SearchField.module.css",
    "content": ".inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.searchIcon {\n  pointer-events: none;\n\n  position: absolute;\n  z-index: 1;\n  left: 12px;\n\n  color: var(--color-text-secondary);\n\n  transition: color 200ms ease;\n}\n\n.input {\n  width: 100%;\n  height: 52px;\n  padding: 15px 16px 15px 62px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 26px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary-reverse);\n\n  background-color: var(--color-bg-primary-reverse);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input::placeholder {\n  font-size: var(--font-size-m);\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/SearchField/SearchField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchField } from './SearchField'\n\nconst meta = {\n  title: 'Components/SearchField',\n  component: SearchField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof SearchField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    placeholder: 'Search for playlists...',\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/SearchField/SearchField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport s from './SearchField.module.css'\n\nexport type SearchFieldProps = {\n  label?: ReactNode\n  placeholder?: string\n} & ComponentProps<'input'>\n\nexport const SearchField = ({\n  className,\n  placeholder = 'Search...',\n  ...props\n}: SearchFieldProps) => {\n  return (\n    <div className={clsx(s.inputWrapper, className)}>\n      <SearchIcon className={s.searchIcon} />\n      <input className={clsx(s.input)} type=\"text\" placeholder={placeholder} {...props} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/SearchField/index.ts",
    "content": "export * from './SearchField'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Select/Select.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.selectWrapper {\n  position: relative;\n  width: 100%;\n}\n\n.select {\n  width: 100%;\n  height: 40px;\n  padding: 8px 36px 8px 12px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-decoration: underline;\n  text-underline-offset: 3px;\n\n  appearance: none;\n  background-color: transparent;\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color;\n}\n\n.select:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.select:focus-visible {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select.error {\n  border-color: var(--color-text-error);\n}\n\n/* Style dropdown options */\n.select option {\n  padding: 8px 12px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: background-color 200ms ease;\n}\n\n.select option:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:checked {\n  font-weight: 600;\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:disabled {\n  color: var(--color-disabled);\n}\n\n/* Custom dropdown icon */\n.icon {\n  pointer-events: none;\n\n  position: absolute;\n  top: 50%;\n  right: 12px;\n  transform: translateY(-50%);\n\n  width: 20px;\n  height: 20px;\n\n  color: var(--color-text-secondary);\n\n  transition:\n    color 200ms ease,\n    transform 200ms ease;\n}\n\n/* Rotate icon when dropdown is open */\n.select:open + .icon {\n  transform: translateY(-50%) rotate(180deg);\n}\n\n.label.error {\n  color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Select/Select.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Select } from './Select'\n\nconst meta = {\n  title: 'Components/Select',\n  component: Select,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Select>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst commonOptions = [\n  { value: 'react', label: 'React' },\n  { value: 'vue', label: 'Vue.js' },\n  { value: 'angular', label: 'Angular' },\n  { value: 'svelte', label: 'Svelte' },\n  { value: 'vanilla', label: 'Vanilla JS' },\n]\n\nconst genres = [\n  { value: 'pop', label: 'Pop' },\n  { value: 'rock', label: 'Rock' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hip-hop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n]\n\nexport const AllVariants = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '350px',\n      }}>\n      <Select label=\"Basic Select\" placeholder=\"Choose option\" options={commonOptions} />\n\n      <Select label=\"With Default Value\" options={commonOptions} defaultValue=\"react\" />\n\n      <Select\n        label=\"With Error\"\n        placeholder=\"Choose option\"\n        options={commonOptions}\n        errorMessage=\"This field is required\"\n      />\n\n      <Select label=\"Disabled\" placeholder=\"Cannot select\" options={commonOptions} disabled />\n    </div>\n  ),\n}\n\nexport const Basic: Story = {\n  args: {\n    label: 'Choose framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDefaultValue: Story = {\n  args: {\n    label: 'Preferred framework',\n    options: commonOptions,\n    defaultValue: 'react',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Framework (disabled)',\n    placeholder: 'Cannot select',\n    options: commonOptions,\n    disabled: true,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithError: Story = {\n  args: {\n    label: 'Framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n    errorMessage: 'Please select a framework',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDisabledOptions: Story = {\n  args: {\n    label: 'Music Genre',\n    placeholder: 'Choose your favorite genre',\n    options: [\n      { value: 'pop', label: 'Pop' },\n      { value: 'rock', label: 'Rock' },\n      { value: 'jazz', label: 'Jazz (Coming Soon)', disabled: true },\n      { value: 'classical', label: 'Classical' },\n      { value: 'electronic', label: 'Electronic (Coming Soon)', disabled: true },\n      { value: 'hip-hop', label: 'Hip Hop' },\n    ],\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Controlled = {\n  render: () => {\n    const [value, setValue] = useState('')\n\n    return (\n      <div style={{ width: '400px', display: 'flex', flexDirection: 'column', gap: '16px' }}>\n        <Select\n          label=\"Music Genre\"\n          placeholder=\"Select genre\"\n          options={genres}\n          value={value}\n          onChange={(e) => setValue(e.target.value)}\n        />\n\n        <div\n          style={{\n            padding: '12px',\n            backgroundColor: 'var(--color-bg-card)',\n            borderRadius: '4px',\n            fontSize: 'var(--font-size-s)',\n            color: 'var(--color-text-secondary)',\n          }}>\n          Selected value: <strong>{value || 'None'}</strong>\n        </div>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Select/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          as=\"label\"\n          htmlFor={selectId}\n          className={clsx(s.label, showError && s.error)}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Select/index.ts",
    "content": "export * from './Select'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Skeleton/Skeleton.module.css",
    "content": ".skeleton {\n  position: relative;\n\n  overflow: hidden;\n  display: inline-block;\n\n  width: 100%;\n  height: 16px;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-secondary);\n}\n\n.skeleton::after {\n  content: '';\n\n  position: absolute;\n  inset: 0;\n  transform: translateX(-100%);\n\n  background: linear-gradient(90deg, transparent, rgb(255 255 255 / 10%), transparent);\n\n  animation: shimmer 1.5s infinite;\n}\n\n.circle {\n  width: 40px;\n  height: 40px;\n  border-radius: 50%;\n}\n\n/* Animation */\n@keyframes shimmer {\n  100% {\n    transform: translateX(100%);\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Skeleton/Skeleton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, CSSProperties } from 'react'\n\nimport s from './Skeleton.module.css'\n\nexport type SkeletonProps = {\n  circle?: boolean\n  width?: number | string\n  height?: number | string\n  className?: string\n  style?: CSSProperties\n} & ComponentProps<'div'>\n\nexport const Skeleton = ({\n  circle = false,\n  width,\n  height,\n  className,\n  style,\n  ...props\n}: SkeletonProps) => {\n  return (\n    <div\n      className={clsx(s.skeleton, circle && s.circle, className)}\n      style={{ width, height, ...style }}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Skeleton/index.ts",
    "content": "export * from './Skeleton'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/SortSelect/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={selectId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Spinner/Spinner.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Spinner } from './Spinner'\n\nconst meta = {\n  argTypes: {\n    size: {\n      control: { type: 'number' },\n    },\n    fullScreen: {\n      control: { type: 'boolean' },\n    },\n  },\n  component: Spinner,\n  tags: ['autodocs'],\n  title: 'Components/Spinner',\n} satisfies Meta<typeof Spinner>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    fullScreen: false,\n    size: 48,\n  },\n}\n\nexport const FullScreen: Story = {\n  args: {\n    fullScreen: true,\n    size: 100,\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Spinner/Spinner.tsx",
    "content": "import type { FC } from 'react'\n\nimport s from './spinner.module.css'\n\nexport type SpinnerProps = {\n  fullScreen?: boolean\n  size?: number\n}\n\nexport const Spinner: FC<SpinnerProps> = ({ fullScreen = false, size = 48 }) => {\n  const containerStyle = {\n    height: fullScreen ? '100vh' : '100%',\n  }\n\n  const style = {\n    height: size,\n    width: size,\n  }\n\n  return (\n    <div className={s.container} style={containerStyle}>\n      <span className={s.loader} style={style} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Spinner/index.ts",
    "content": "export * from './Spinner.tsx'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Spinner/spinner.module.css",
    "content": ".container {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 100%;\n  height: 100%;\n}\n\n.loader {\n  display: inline-block;\n\n  box-sizing: border-box;\n\n  border-top: 3px solid var(--color-accent);\n  border-right: 3px solid transparent;\n  border-radius: 50%;\n\n  animation: rotation 1s linear infinite;\n}\n\n@keyframes rotation {\n  0% {\n    transform: rotate(0deg);\n  }\n\n  100% {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Table/Table.module.css",
    "content": ".table {\n  table-layout: fixed;\n  border-collapse: collapse;\n  width: 100%;\n  background: transparent;\n}\n\n.tableHead {\n  border-bottom: 1px solid var(--color-border-base);\n}\n\n.tableHeaderCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-xs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  text-align: left;\n  text-transform: uppercase;\n\n  background: transparent;\n}\n\n.tableHeaderCell:first-child {\n  padding-left: 16px;\n}\n\n.tableHeaderCell:last-child {\n  padding-right: 16px;\n}\n\n.tableBody {\n  background: transparent;\n}\n\n.tableRow {\n  transition: background-color 200ms ease;\n}\n\n.tableBody .tableRow:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tableCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  vertical-align: middle;\n\n  background: transparent;\n}\n\n.tableCell:first-child {\n  padding-left: 16px;\n}\n\n.tableCell:last-child {\n  padding-right: 16px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Table/Table.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ReactionButtons } from '../ReactionButtons'\nimport { Typography } from '../Typography'\nimport { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './Table'\nimport s from './Table.module.css'\n\nconst meta = {\n  title: 'Components/Table',\n  component: Table,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Table>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst trackData = [\n  {\n    id: 1,\n    title: 'Play It Safe',\n    artist: 'Julia Wolf',\n    image: 'https://picsum.photos/40/40?random=1',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 2,\n    title: 'Ocean Front Apt.',\n    artist: 'ayokay',\n    image: 'https://picsum.photos/40/40?random=2',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 3,\n    title: 'Free Spirit',\n    artist: 'Khalid',\n    image: 'https://picsum.photos/40/40?random=3',\n    dateAdded: '2 day ago',\n    duration: '3:02',\n  },\n  {\n    id: 4,\n    title: 'Remind You',\n    artist: 'FRENSHIP',\n    image: 'https://picsum.photos/40/40?random=4',\n    dateAdded: '3 day ago',\n    duration: '4:25',\n  },\n]\n\nexport const BasicTable = {\n  render: () => (\n    <div style={{ width: '600px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Name</TableHeaderCell>\n            <TableHeaderCell>Email</TableHeaderCell>\n            <TableHeaderCell>Role</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell>John Doe</TableCell>\n            <TableCell>john@example.com</TableCell>\n            <TableCell>Admin</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Jane Smith</TableCell>\n            <TableCell>jane@example.com</TableCell>\n            <TableCell>User</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Bob Johnson</TableCell>\n            <TableCell>bob@example.com</TableCell>\n            <TableCell>Editor</TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n\nexport const EmptyTable = {\n  render: () => (\n    <div style={{ width: '500px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Column&nbsp;1</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;2</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;3</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell colSpan={3}>\n              <Typography variant=\"body2\" style={{ textAlign: 'center', padding: '40px 20px' }}>\n                No data available\n              </Typography>\n            </TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Table/Table.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport s from './Table.module.css'\n\n/*\n * Table\n */\n\nexport type TableProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'table'>\n\nexport const Table = ({ children, className, ...props }: TableProps) => {\n  return (\n    <table className={clsx(s.table, className)} {...props}>\n      {children}\n    </table>\n  )\n}\n\n/*\n * TableHead\n */\n\nexport type TableHeadProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'thead'>\n\nexport const TableHead = ({ children, className, ...props }: TableHeadProps) => {\n  return (\n    <thead className={clsx(s.tableHead, className)} {...props}>\n      {children}\n    </thead>\n  )\n}\n\n/*\n * TableBody\n */\n\nexport type TableBodyProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tbody'>\n\nexport const TableBody = ({ children, className, ...props }: TableBodyProps) => {\n  return (\n    <tbody className={clsx(s.tableBody, className)} {...props}>\n      {children}\n    </tbody>\n  )\n}\n\n/*\n * TableRow\n */\n\nexport type TableRowProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tr'>\n\nexport const TableRow = ({ children, className, ...props }: TableRowProps) => {\n  return (\n    <tr className={clsx(s.tableRow, className)} {...props}>\n      {children}\n    </tr>\n  )\n}\n\n/*\n * TableHeaderCell\n */\n\nexport type TableHeaderCellProps = {\n  children?: ReactNode\n  className?: string\n} & ComponentProps<'th'>\n\nexport const TableHeaderCell = ({ children, className, ...props }: TableHeaderCellProps) => {\n  return (\n    <th className={clsx(s.tableHeaderCell, className)} {...props}>\n      {children}\n    </th>\n  )\n}\n\n/*\n * TableCell\n */\n\nexport type TableCellProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'td'>\n\nexport const TableCell = ({ children, className, ...props }: TableCellProps) => {\n  return (\n    <td className={clsx(s.tableCell, className)} {...props}>\n      {children}\n    </td>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Table/index.ts",
    "content": "export * from './Table'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Tabs/Tabs.module.css",
    "content": ".tabsList {\n  display: flex;\n  width: 100%;\n  border-bottom: 1px solid var(--color-text-secondary);\n}\n\n.tabsTrigger {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  flex: 1 1 0;\n  align-items: center;\n  justify-content: center;\n\n  padding: 10px 16px;\n  border: none;\n\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.tabsTrigger:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.tabsTrigger:not(.active, :disabled):hover {\n  opacity: 0.7;\n}\n\n.tabsTrigger.active {\n  color: var(--color-accent);\n}\n\n.tabsTrigger.active::after {\n  content: '';\n\n  position: absolute;\n  bottom: -1px;\n  left: 0;\n\n  width: 100%;\n  height: 2px;\n\n  background-color: var(--color-accent);\n}\n\n.tabsTrigger.disabled {\n  cursor: default;\n  color: var(--color-disabled);\n}\n\n.tabsContent {\n  padding: 24px 0;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Tabs/Tabs.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs'\n\nconst meta = {\n  title: 'Components/Tabs',\n  component: Tabs,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Tabs>\n\nexport default meta\n\nexport const BasicTabs = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Tabs defaultValue=\"account\">\n        <TabsList>\n          <TabsTrigger value=\"account\">Account</TabsTrigger>\n          <TabsTrigger value=\"password\">Password</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"account\">\n          <Typography variant=\"body1\">Make changes to your account here.</Typography>\n        </TabsContent>\n        <TabsContent value=\"password\">\n          <Typography variant=\"body1\">Change your password here.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n\nexport const ControlledTabs = {\n  render: () => {\n    const [activeTab, setActiveTab] = useState('tab1')\n\n    return (\n      <div style={{ width: '500px' }}>\n        <Tabs value={activeTab} onValueChange={setActiveTab}>\n          <TabsList>\n            <TabsTrigger value=\"tab1\">Tab 1</TabsTrigger>\n            <TabsTrigger value=\"tab2\">Tab 2</TabsTrigger>\n            <TabsTrigger value=\"tab3\">Tab 3</TabsTrigger>\n          </TabsList>\n          <TabsContent value=\"tab1\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              First Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the first tab. You can put any React content here.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab2\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Second Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the second tab with different information.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab3\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Third Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              And this is the third tab with its own unique content.\n            </Typography>\n          </TabsContent>\n        </Tabs>\n\n        <Card\n          style={{\n            marginTop: '20px',\n          }}>\n          <Typography variant=\"body2\">\n            Active tab: <strong>{activeTab}</strong>\n          </Typography>\n          <div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab1')}>\n              Go to Tab 1\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab2')}>\n              Go to Tab 2\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab3')}>\n              Go to Tab 3\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const DisabledTab = {\n  render: () => (\n    <div style={{ width: '350px' }}>\n      <Tabs defaultValue=\"available\">\n        <TabsList>\n          <TabsTrigger value=\"available\">Available</TabsTrigger>\n          <TabsTrigger value=\"disabled\" disabled>\n            Disabled\n          </TabsTrigger>\n          <TabsTrigger value=\"another\">Another</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"available\">\n          <Typography variant=\"body1\">This tab is available and active.</Typography>\n        </TabsContent>\n        <TabsContent value=\"disabled\">\n          <Typography variant=\"body1\">This content should not be visible.</Typography>\n        </TabsContent>\n        <TabsContent value=\"another\">\n          <Typography variant=\"body1\">This is another available tab.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Tabs/Tabs.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ComponentProps, createContext, type ReactNode, use, useState } from 'react'\n\nimport s from './Tabs.module.css'\n\ntype TabsContextType = {\n  value?: string\n  onValueChange?: (value: string) => void\n}\n\nconst TabsContext = createContext<TabsContextType | null>(null)\n\nconst useTabsContext = () => {\n  const context = use(TabsContext)\n  if (!context) {\n    throw new Error('Tabs compound components must be used within Tabs component')\n  }\n  return context\n}\n\n/*\n * Tabs\n */\n\nexport type TabsProps = {\n  children: ReactNode\n  defaultValue?: string\n  value?: string\n  onValueChange?: (value: string) => void\n} & ComponentProps<'div'>\n\nexport const Tabs = ({\n  children,\n  defaultValue,\n  value: controlledValue,\n  onValueChange,\n  className,\n  ...props\n}: TabsProps) => {\n  const [internalValue, setInternalValue] = useState(defaultValue)\n\n  const isControlled = controlledValue !== undefined\n  const value = isControlled ? controlledValue : internalValue\n\n  const handleValueChange = (newValue: string) => {\n    if (!isControlled) {\n      setInternalValue(newValue)\n    }\n    onValueChange?.(newValue)\n  }\n\n  return (\n    <div className={className} {...props}>\n      <TabsContext value={{ value, onValueChange: handleValueChange }}>{children}</TabsContext>\n    </div>\n  )\n}\n\n/*\n * TabsList\n */\n\nexport type TabsListProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const TabsList = ({ children, className }: TabsListProps) => {\n  return <div className={clsx(s.tabsList, className)}>{children}</div>\n}\n\n/*\n * TabsTrigger\n */\n\nexport type TabsTriggerProps = {\n  children: ReactNode\n  value: string\n  className?: string\n  disabled?: boolean\n}\n\nexport const TabsTrigger = ({ children, value, className, disabled }: TabsTriggerProps) => {\n  const { value: activeValue, onValueChange } = useTabsContext()\n  const isActive = activeValue === value\n\n  const handleClick = () => {\n    if (!disabled) {\n      onValueChange?.(value)\n    }\n  }\n\n  return (\n    <button\n      className={clsx(s.tabsTrigger, isActive && s.active, disabled && s.disabled, className)}\n      onClick={handleClick}\n      disabled={disabled}\n      type=\"button\">\n      {children}\n    </button>\n  )\n}\n\n/*\n * TabsContent\n */\n\nexport type TabsContentProps = {\n  children: ReactNode\n  value: string\n  className?: string\n}\n\nexport const TabsContent = ({ children, value, className }: TabsContentProps) => {\n  const { value: activeValue } = useTabsContext()\n  const isActive = activeValue === value\n\n  if (!isActive) return null\n\n  return <div className={clsx(s.tabsContent, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Tabs/index.ts",
    "content": "export * from './Tabs'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/TagEditor/TagEditor.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n}\n\n.tagsContainer {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n\n  margin-top: 12px;\n  padding: 8px 0;\n}\n\n.tag {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n\n  padding: 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 16px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.deleteButton:enabled:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.counter {\n  margin-top: 8px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/TagEditor/TagEditor.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { TagEditor } from './TagEditor'\n\nconst meta = {\n  title: 'Components/TagEditor',\n  component: TagEditor,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TagEditor>\n\nexport default meta\n\nexport const Basic = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags\"\n          placeholder=\"Add tag and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Skills (max 5)\"\n          placeholder=\"Add skill and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={5}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [tags, setTags] = useState(['React', 'TypeScript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags (disabled)\"\n          placeholder=\"Cannot add tags\"\n          value={tags}\n          onTagsChange={setTags}\n          disabled={true}\n        />\n      </div>\n    )\n  },\n}\n\nexport const PrefilledTags = {\n  render: () => {\n    const [tags, setTags] = useState([\n      'JavaScript',\n      'TypeScript',\n      'React',\n      'Node.js',\n      'CSS',\n      'HTML',\n    ])\n\n    return (\n      <div style={{ width: '450px' }}>\n        <TagEditor\n          label=\"Programming Languages & Technologies\"\n          placeholder=\"Add more technologies...\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={10}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendTags, setFrontendTags] = useState(['React', 'Vue.js'])\n    const [backendTags, setBackendTags] = useState(['Node.js'])\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Frontend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Frontend\"\n            placeholder=\"Add frontend technology...\"\n            value={frontendTags}\n            onTagsChange={setFrontendTags}\n            maxTags={8}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Backend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Backend\"\n            placeholder=\"Add backend technology...\"\n            value={backendTags}\n            onTagsChange={setBackendTags}\n            maxTags={6}\n          />\n        </div>\n\n        <Card>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Summary:\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '4px' }}>\n            Frontend: {frontendTags.length > 0 ? frontendTags.join(', ') : 'None'}\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block' }}>\n            Backend: {backendTags.length > 0 ? backendTags.join(', ') : 'None'}\n          </Typography>\n        </Card>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/TagEditor/TagEditor.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, KeyboardEvent } from 'react'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\n\nimport { DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport s from './TagEditor.module.css'\n\nexport type TagEditorProps = {\n  label?: string\n  placeholder?: string\n  value: string[]\n  onTagsChange: (tags: string[]) => void\n  maxTags?: number\n  disabled?: boolean\n} & ComponentProps<'div'>\n\nexport const TagEditor = ({\n  label,\n  placeholder,\n  value,\n  onTagsChange,\n  className,\n  maxTags,\n  disabled = false,\n  ...props\n}: TagEditorProps) => {\n  const { t } = useTranslation()\n  const [inputValue, setInputValue] = useState('')\n  const defaultPlaceholder = placeholder || t('tags.add_tag_placeholder')\n\n  const addTag = (tag: string) => {\n    const trimmedTag = tag.trim()\n\n    if (!trimmedTag) {\n      return\n    }\n    if (value.includes(trimmedTag)) {\n      return\n    }\n    if (maxTags && value.length >= maxTags) {\n      return\n    }\n\n    onTagsChange([...value, trimmedTag])\n    setInputValue('')\n  }\n\n  const removeTag = (tagToRemove: string) => {\n    onTagsChange(value.filter((tag) => tag !== tagToRemove))\n  }\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      addTag(inputValue)\n    }\n\n    if (e.key === 'Backspace' && !inputValue && value.length > 0) {\n      removeTag(value[value.length - 1])\n    }\n  }\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <TextField\n        label={label}\n        value={inputValue}\n        onChange={(e) => setInputValue(e.target.value)}\n        onKeyDown={handleKeyDown}\n        placeholder={isMaxTagsReached ? 'Max tags reached' : defaultPlaceholder}\n        disabled={disabled}\n      />\n\n      {value.length > 0 && (\n        <ul className={s.tagsContainer}>\n          {value.map((tag) => (\n            <li key={tag} className={s.tag}>\n              <Typography variant=\"body2\" className={s.tagText}>\n                {tag}\n              </Typography>\n              <IconButton\n                onClick={() => removeTag(tag)}\n                className={s.deleteButton}\n                disabled={disabled}\n                aria-label={`Remove tag ${tag}`}\n                type=\"button\">\n                <DeleteIcon />\n              </IconButton>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} tags\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/TagEditor/index.ts",
    "content": "export * from './TagEditor'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/TextField/TextField.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.icon {\n  position: absolute;\n  top: 50%;\n  left: 12px;\n  transform: translateY(-50%);\n\n  display: flex;\n\n  color: var(--color-text-secondary);\n}\n\n.input {\n  width: 100%;\n  height: 40px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input.large {\n  height: 56px;\n}\n\n.input:disabled {\n  color: var(--color-disabled);\n}\n\n.input:focus,\n.input:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.input:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input.error {\n  border-color: var(--color-text-error);\n}\n\n.input.withIcon {\n  padding-left: 40px;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/TextField/TextField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport { TextField } from './TextField'\n\nconst meta = {\n  title: 'Components/TextField',\n  component: TextField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TextField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const Search: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    icon: <SearchIcon width={20} height={20} />,\n    inputSize: 'l',\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/TextField/TextField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks'\nimport { Typography } from '../Typography'\nimport s from './TextField.module.css'\n\nexport type TextFieldSize = 'm' | 'l'\n\nexport type TextFieldProps = {\n  errorMessage?: string\n  label?: ReactNode\n  icon?: ReactNode\n  inputSize?: TextFieldSize\n} & ComponentProps<'input'>\n\nexport const TextField = ({\n  className,\n  errorMessage,\n  id,\n  icon,\n  label,\n  inputSize = 'm',\n  ...props\n}: TextFieldProps) => {\n  const showError = Boolean(errorMessage)\n  const inputId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={inputId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.inputWrapper}>\n        {icon && <span className={s.icon}>{icon}</span>}\n        <input\n          className={clsx(\n            s.input,\n            showError && s.error,\n            icon && s.withIcon,\n            inputSize === 'l' && s.large\n          )}\n          id={inputId}\n          type={'text'}\n          {...props}\n        />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/TextField/index.ts",
    "content": "export * from './TextField'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Textarea/Textarea.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.textarea {\n  resize: none;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.textarea:disabled {\n  color: var(--color-disabled);\n}\n\n.textarea:focus,\n.textarea:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.textarea:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.textarea::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.textarea.error {\n  border-color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Textarea/Textarea.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Textarea } from './Textarea'\n\nconst meta = {\n  title: 'Components/Textarea',\n  component: Textarea,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Textarea>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const WithRows: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    rows: 5,\n  },\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Textarea/Textarea.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks'\nimport { Typography } from '../Typography'\nimport s from './Textarea.module.css'\n\nexport type TextareaProps = {\n  errorMessage?: string\n  label?: ReactNode\n} & ComponentProps<'textarea'>\n\nexport const Textarea = ({ className, errorMessage, id, label, ...props }: TextareaProps) => {\n  const showError = Boolean(errorMessage)\n  const textareaId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={textareaId}>\n          {label}\n        </Typography>\n      )}\n\n      <textarea className={clsx(s.textarea, showError && s.error)} id={textareaId} {...props} />\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Textarea/index.ts",
    "content": "export * from './Textarea'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Typography/Typography.module.css",
    "content": ".label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.error {\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.h1 {\n  font-size: var(--font-size-xxxl);\n}\n\n.h2 {\n  margin: 0;\n  font-size: var(--font-size-xl);\n  font-weight: 600;\n  line-height: 1.3;\n}\n\n.h3 {\n  margin: 0;\n  font-size: var(--font-size-xs);\n  font-weight: 600;\n  line-height: 1.7;\n}\n\n.body1 {\n  margin: 0;\n  font-size: var(--font-size-l);\n  font-weight: 400;\n}\n\n.body2 {\n  margin: 0;\n  font-size: var(--font-size-m);\n  font-weight: 400;\n  color: var(--color-text-secondary);\n}\n\n.body3 {\n  margin: 0;\n  font-size: var(--font-size-xxs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* ------------------------------------------------------------ */\n\n.caption {\n  margin: 0;\n  font-size: 0.75rem;\n  font-weight: 400;\n  line-height: 1.66;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Typography/Typography.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from './Typography'\n\nconst meta = {\n  title: 'Components/Typography',\n  component: Typography,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Typography>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllTypography: Story = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>\n      <Typography variant=\"h1\">h1</Typography>\n      <Typography variant=\"h2\">h2</Typography>\n      <Typography variant=\"h3\">h3</Typography>\n      <Typography variant=\"body1\">body1</Typography>\n      <Typography variant=\"body2\">body2</Typography>\n      <Typography variant=\"caption\">caption</Typography>\n      <Typography variant=\"label\">label</Typography>\n      <Typography variant=\"error\">error</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Typography/Typography.tsx",
    "content": "import clsx from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\nimport React from 'react'\n\nimport styles from './Typography.module.css'\n\nconst VARIANT_DEFAULT_COMPONENT: Record<string, ElementType> = {\n  h1: 'h1',\n  h2: 'h2',\n  h3: 'h3',\n  body1: 'p',\n  body2: 'p',\n  body3: 'p',\n  caption: 'span',\n  label: 'label',\n}\n\ntype TypographyVariant =\n  | 'h1'\n  | 'h2'\n  | 'h3'\n  | 'body1'\n  | 'body2'\n  | 'body3'\n  | 'caption'\n  | 'label'\n  | 'error'\n\ntype Props<T extends ElementType> = {\n  variant?: TypographyVariant\n  as?: T\n  children: React.ReactNode\n} & ComponentProps<T>\n\nexport const Typography = <T extends ElementType = 'span'>({\n  variant = 'body1',\n  as,\n  children,\n  className = '',\n  ...props\n}: Props<T>) => {\n  const Component = as || VARIANT_DEFAULT_COMPONENT[variant] || 'span'\n  const variantClass = styles[variant] || ''\n\n  return (\n    <Component className={clsx(variantClass, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/Typography/index.ts",
    "content": "export * from './Typography'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/components/index.ts",
    "content": "export * from './AudioPlayer'\nexport * from './Avatar'\nexport * from './Autocomplete'\nexport * from './Button'\nexport * from './Card'\nexport * from './CoverImage'\nexport * from './Dialog'\nexport * from './DropdownMenu'\nexport * from './FormControlledTextField'\nexport * from './Hashtag'\nexport * from './IconButton'\nexport * from './ImageCropper'\nexport * from './ImageUploader'\nexport * from './LanguageSwitcher'\nexport * from './Pagination'\nexport * from './Progress'\nexport * from './ReactionButtons'\nexport * from './SearchField'\nexport * from './Select'\nexport * from './Skeleton'\nexport * from './Spinner'\nexport * from './Table'\nexport * from './Tabs'\nexport * from './TagEditor'\nexport * from './Textarea'\nexport * from './TextField'\nexport * from './Typography'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/config/config.ts",
    "content": "export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL\nexport const API_KEY = import.meta.env.VITE_API_KEY\nexport const CURRENT_APP_DOMAIN = import.meta.env.VITE_APP_BASE_URL\n\nconsole.log('CURRENT_APP_DOMAIN: ', CURRENT_APP_DOMAIN)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/config/paths.ts",
    "content": "export const Paths = {\n  Main: '/',\n  Playlists: '/playlists',\n  Profile: '/user',\n  Tracks: '/tracks',\n  OAuthRedirect: '/oauth/callback',\n} as const\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/featureFlags.ts",
    "content": "export const featuresFlags = {\n  deletePlaylist: true,\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/debounceCallback/index.ts",
    "content": "export { default } from './useDebounceCallback'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/debounceCallback/useDebounceCallback.ts",
    "content": "import * as React from 'react'\n\nimport type { DebouncedState } from './useDebounceCallback.types.ts'\n\n/**\n * Custom hook for debouncing a function — delays its execution until a specified\n * number of milliseconds have passed since the last time it was invoked.\n *\n * Returns a wrapped function with additional control methods:\n * - `isPending()` — checks whether a debounced call is currently scheduled\n * - `cancel()`    — cancels the scheduled call\n * - `flush()`     — immediately executes the function (if it was scheduled)\n *\n * @template F Type of the function being debounced\n * @param {F} callback The function to debounce\n * @param {number} [delay=300] Delay in milliseconds\n * @returns {DebouncedState<F>} Debounced function with control methods\n *\n * @example\n * const debouncedSearch = useDebounceCallback((query: string) => {\n *   fetchUsers(query);\n * }, 500);\n *\n * // Calls will be delayed by 500 ms\n * debouncedSearch('react');\n * debouncedSearch('react hook'); // previous call will be cancelled\n *\n * if (debouncedSearch.isPending()) {\n *   console.log('Request has not been sent yet...');\n *   debouncedSearch.flush(); // send immediately\n * }\n *\n * debouncedSearch.cancel(); // cancel completely\n */\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction useDebounceCallback<F extends (...args: any[]) => ReturnType<F>>(\n  callback: F,\n  delay = 300\n): DebouncedState<F> {\n  const timerRef = React.useRef<null | ReturnType<typeof setTimeout>>(null)\n  const argsRef = React.useRef<Parameters<F> | null>(null)\n  const callbackRef = React.useRef(callback)\n\n  callbackRef.current = callback\n\n  React.useEffect(() => {\n    return () => {\n      if (timerRef.current != null) {\n        clearTimeout(timerRef.current)\n      }\n    }\n  }, [delay])\n\n  return React.useMemo(() => {\n    const debounced = (...args: Parameters<F>) => {\n      argsRef.current = args\n\n      if (timerRef.current != null) {\n        clearTimeout(timerRef.current)\n      }\n\n      timerRef.current = setTimeout(() => {\n        timerRef.current = null\n        const argsToUse = argsRef.current\n        argsRef.current = null\n\n        if (argsToUse != null) {\n          callbackRef.current?.(...argsToUse)\n        }\n      }, delay)\n    }\n\n    const func = debounced as DebouncedState<F>\n\n    func.isPending = () => {\n      return !!timerRef.current\n    }\n\n    func.cancel = () => {\n      if (timerRef.current != null) {\n        clearTimeout(timerRef.current)\n        timerRef.current = null\n      }\n    }\n\n    func.flush = () => {\n      if (timerRef.current != null) {\n        clearTimeout(timerRef.current)\n        timerRef.current = null\n      }\n      if (argsRef.current != null) {\n        const args = argsRef.current\n        argsRef.current = null\n        callbackRef.current?.(...args)\n      }\n    }\n\n    return func\n  }, [delay])\n}\n\nexport default useDebounceCallback\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/debounceCallback/useDebounceCallback.types.ts",
    "content": "interface ControlFunctions {\n  isPending(): boolean\n\n  cancel(): void\n\n  flush(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type DebouncedState<F extends (...args: any[]) => ReturnType<F>> = ((\n  ...args: Parameters<F>\n) => void) &\n  ControlFunctions\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/debounceValue/index.ts",
    "content": "export { default } from './useDebounceValue.ts'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/debounceValue/useDebounceValue.ts",
    "content": "import { useEffect, useState } from 'react'\n\nconst useDebounceValue = <T>(value: T, delay: number = 700): [T] => {\n  const [debounced, setDebounced] = useState(value)\n\n  useEffect(() => {\n    const handler = setTimeout(() => setDebounced(value), delay)\n    return () => clearTimeout(handler)\n  }, [value, delay])\n\n  return [debounced]\n}\n\nexport default useDebounceValue\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/getId/index.ts",
    "content": "export { default } from './useGetId'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/getId/useGetId.ts",
    "content": "import { useId } from 'react'\n\n/*\n * Custom hook to get an ID.\n * If an ID is passed from component props, it returns that ID.\n * Otherwise, it generates and returns a new unique ID.\n *\n * @param {string} [idFromComponentProps] - An optional ID passed from ComponentProps.\n * @returns {string} The ID from component props or a generated unique ID.\n */\nconst useGetId = (idFromComponentProps?: string) => {\n  const generatedId = useId()\n\n  return idFromComponentProps || generatedId\n}\n\nexport default useGetId\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/index.ts",
    "content": "export { default as useDebounceCallback } from './debounceCallback'\nexport { default as useDebounceValue } from './debounceValue'\nexport { default as useGetId } from './getId'\nexport { default as useThrottleCallback } from './throttleCallback'\nexport * from './useHover'\nexport * from './useCurrentPage'\nexport * from './usePageSearchParams'\nexport * from './usePageBackgroundColor'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/throttleCallback/index.ts",
    "content": "export { default } from './useThrottleCallback'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/throttleCallback/useThrottleCallback.tsx",
    "content": "import * as React from 'react'\n\nimport type { ThrottledState, ThrottleOptions } from './useThrottleCallback.types.ts'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction useThrottleCallback<F extends (...args: any[]) => ReturnType<F>>(\n  callback: F,\n  delay: number = 300,\n  options: ThrottleOptions = {}\n): ThrottledState<F> {\n  const { leading = true, trailing = true } = options\n\n  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n  const lastExecTimeRef = React.useRef<number>(0)\n  const pendingArgsRef = React.useRef<Parameters<F> | null>(null)\n  const callbackRef = React.useRef(callback)\n\n  callbackRef.current = callback\n\n  React.useEffect(() => {\n    return () => {\n      if (timerRef.current != null) {\n        clearTimeout(timerRef.current)\n        timerRef.current = null\n      }\n    }\n  }, [delay])\n\n  return React.useMemo(() => {\n    const execute = (args: Parameters<F>) => {\n      callbackRef.current?.(...args)\n      lastExecTimeRef.current = Date.now()\n    }\n\n    const throttled = (...args: Parameters<F>) => {\n      const now = Date.now()\n      const elapsed = now - lastExecTimeRef.current\n\n      if (!leading && lastExecTimeRef.current === 0) {\n        lastExecTimeRef.current = now\n      }\n\n      const remaining = delay - elapsed\n\n      if (trailing) {\n        pendingArgsRef.current = args\n      }\n\n      if (remaining <= 0 || !leading) {\n        if (timerRef.current != null) {\n          clearTimeout(timerRef.current)\n          timerRef.current = null\n        }\n\n        if (leading || lastExecTimeRef.current !== 0) {\n          execute(args)\n        }\n        return\n      }\n\n      if (timerRef.current == null) {\n        timerRef.current = setTimeout(() => {\n          timerRef.current = null\n\n          if (trailing && pendingArgsRef.current != null) {\n            const argsToUse = pendingArgsRef.current\n            pendingArgsRef.current = null\n            execute(argsToUse)\n          }\n        }, remaining)\n      }\n    }\n\n    const func = throttled as ThrottledState<F>\n\n    func.isPending = () => {\n      return timerRef.current != null || pendingArgsRef.current != null\n    }\n\n    func.cancel = () => {\n      if (timerRef.current != null) {\n        clearTimeout(timerRef.current)\n        timerRef.current = null\n      }\n      pendingArgsRef.current = null\n    }\n\n    func.flush = () => {\n      if (pendingArgsRef.current != null) {\n        const args = pendingArgsRef.current\n        pendingArgsRef.current = null\n\n        if (timerRef.current != null) {\n          clearTimeout(timerRef.current)\n          timerRef.current = null\n        }\n\n        execute(args)\n      }\n    }\n\n    return func\n  }, [delay, leading, trailing])\n}\n\nexport default useThrottleCallback\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/throttleCallback/useThrottleCallback.types.ts",
    "content": "interface ThrottleControlFunctions {\n  isPending(): boolean\n\n  cancel(): void\n\n  flush(): void\n}\n\nexport interface ThrottleOptions {\n  leading?: boolean\n  trailing?: boolean\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ThrottledState<F extends (...args: any[]) => ReturnType<F>> = ((\n  ...args: Parameters<F>\n) => void) &\n  ThrottleControlFunctions\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/useCurrentPage.ts",
    "content": "import { useLocation } from 'react-router'\n\nexport const useCurrentPage = () => {\n  const { pathname } = useLocation()\n\n  const isTrackPage = pathname.includes('/tracks/')\n  const isPlaylistPage = pathname.includes('/playlists/')\n\n  return {\n    isTrackPage,\n    isPlaylistPage,\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/useDeletePlaylistAction.ts",
    "content": "import { toast } from 'react-toastify'\n\nimport { useDeletePlaylist } from '@/pages/PlaylistsPage/model/useDeletePlaylist'\n\n/*for delete Playlist into Playlist modal and PlaylistCard*/\nexport const useDeletePlaylistAction = (playlistId: string) => {\n  const { mutate } = useDeletePlaylist()\n\n  return () => {\n    if (confirm('Do you want to delete the playlist?')) {\n      mutate(playlistId, {\n        onSuccess: () => {\n          toast('Playlist has been deleted', { type: 'success', theme: 'colored' })\n        },\n        onError: () => {\n          toast('Failed to delete playlist', { type: 'error', theme: 'colored' })\n        },\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/useEntityReactions.ts",
    "content": "import { type Query, type QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport type {\n  SchemaGetTrackListOutput,\n  SchemaReactionOutput,\n  SchemaTrackListItemResource,\n} from '@/shared/api/schema'\nimport { tracksKeys } from '@/features/tracks/api/query-key-factory.ts'\n\ninterface UseEntityReactionsConfig {\n  entityId: SchemaReactionOutput['objectId']\n  keys: {\n    all: readonly unknown[]\n  }\n  api: {\n    like: (id: string) => Promise<any>\n    dislike: (id: string) => Promise<any>\n    remove: (id: string) => Promise<any>\n  }\n}\n\ntype Track = SchemaTrackListItemResource\ntype TrackPage = SchemaGetTrackListOutput\n\nexport function useEntityReactions({ entityId, api, keys }: UseEntityReactionsConfig) {\n  const queryClient = useQueryClient()\n\n  const commonOptimisticUpdate = async (action: 'like' | 'dislike' | 'remove') => {\n    const tracksInfinitePredicate = (query: Query) => {\n      const queryKey = query.queryKey\n      return (\n        queryKey[0] === tracksKeys.all[0] && queryKey[1] === 'list' && queryKey[2] === 'infinite'\n      )\n    }\n\n    await queryClient.cancelQueries({ predicate: tracksInfinitePredicate })\n\n    const previousData = queryClient\n      .getQueryCache()\n      .findAll({\n        predicate: tracksInfinitePredicate,\n      })\n      .map((q) => ({\n        key: q.queryKey,\n        data: queryClient.getQueryData<any>(q.queryKey),\n      }))\n\n    previousData.forEach(({ key }) => {\n      queryClient.setQueryData<{\n        pages: TrackPage[]\n        pageParams: any[]\n      }>(key, (old) => {\n        if (!old?.pages) return old\n\n        return {\n          ...old,\n          pages: old.pages.map((page: TrackPage) => ({\n            ...page,\n            data: page.data.map((track: Track) => {\n              if (track.id !== entityId) return track\n\n              const currentReaction = track.attributes.currentUserReaction ?? 0\n              let likesCount = track.attributes.likesCount ?? 0\n              let newReaction = currentReaction\n\n              if (action === 'like') {\n                if (currentReaction === 1) {\n                  likesCount -= 1\n                  newReaction = 0\n                } else {\n                  if (currentReaction === -1) {\n                    likesCount += 1\n                  } else {\n                    likesCount += 1\n                  }\n                  newReaction = 1\n                }\n              } else if (action === 'dislike') {\n                if (currentReaction === -1) {\n                  newReaction = 0\n                } else {\n                  if (currentReaction === 1) {\n                    likesCount -= 1\n                  }\n                  newReaction = -1\n                }\n              } else if (action === 'remove') {\n                if (currentReaction === 1) {\n                  likesCount -= 1\n                }\n\n                newReaction = 0\n              }\n\n              return {\n                ...track,\n                attributes: {\n                  ...track.attributes,\n                  likesCount,\n                  currentUserReaction: newReaction,\n                },\n              }\n            }),\n          })),\n        }\n      })\n    })\n\n    return { previousData }\n  }\n\n  const commonErrorHandler = (\n    context: { previousData?: Array<{ key: QueryKey; data: any }> } | undefined\n  ) => {\n    if (context?.previousData) {\n      context.previousData.forEach(({ key, data }) => {\n        queryClient.setQueryData(key, data)\n      })\n    }\n  }\n\n  const commonSuccessHandler = () => {\n    queryClient.invalidateQueries({\n      queryKey: keys.all,\n    })\n  }\n\n  const like = useMutation({\n    mutationFn: () => api.like(entityId),\n    onMutate: () => commonOptimisticUpdate('like'),\n    onError: commonErrorHandler,\n    onSuccess: commonSuccessHandler,\n  })\n\n  const dislike = useMutation({\n    mutationFn: () => api.dislike(entityId),\n    onMutate: () => commonOptimisticUpdate('dislike'),\n    onError: commonErrorHandler,\n    onSuccess: commonSuccessHandler,\n  })\n\n  const remove = useMutation({\n    mutationFn: () => api.remove(entityId),\n    onMutate: () => commonOptimisticUpdate('remove'),\n    onError: commonErrorHandler,\n    onSuccess: commonSuccessHandler,\n  })\n\n  return {\n    handleLike: () => (like.mutate as () => void)(),\n    handleDislike: () => (dislike.mutate as () => void)(),\n    handleRemoveReaction: () => (remove.mutate as () => void)(),\n    isPending: like.isPending || dislike.isPending || remove.isPending,\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/useHover.ts",
    "content": "import { type RefObject, useEffect, useRef, useState } from 'react'\n\nexport function useHover<T extends HTMLElement>(): [RefObject<T | null>, boolean] {\n  const [hover, setHover] = useState(false)\n  const ref = useRef<T | null>(null)\n\n  useEffect(() => {\n    const node = ref.current\n    if (!node) return\n\n    const handleMouseEnter = () => setHover(true)\n    const handleMouseLeave = () => setHover(false)\n\n    node.addEventListener('mouseenter', handleMouseEnter)\n    node.addEventListener('mouseleave', handleMouseLeave)\n\n    return () => {\n      node.removeEventListener('mouseenter', handleMouseEnter)\n      node.removeEventListener('mouseleave', handleMouseLeave)\n    }\n  }, [])\n\n  return [ref, hover]\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/usePageBackgroundColor.ts",
    "content": "import { useEffect, useRef, useState } from 'react'\n\nconst DEFAULT_BACKGROUND_COLOR = '#3333a3'\n\nexport const usePageBackgroundColor = (\n  url: string | null | undefined,\n  isSuccess: boolean,\n  isLocalUrlData?: boolean\n) => {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null)\n  const [dominantColor, setDominantColor] = useState<string>('')\n\n  useEffect(() => {\n    if (isSuccess && !url) {\n      setDominantColor(DEFAULT_BACKGROUND_COLOR)\n    }\n    if (url) {\n      const img = new Image()\n      img.crossOrigin = 'anonymous'\n      img.src = isLocalUrlData ? url : url + '?' //to avoid CORS error\n      img.onload = () => {\n        if (isLocalUrlData) {\n          URL.revokeObjectURL(url)\n        }\n        const canvas = canvasRef.current\n        if (canvas) {\n          canvas.width = img.naturalWidth\n          canvas.height = img.naturalHeight\n          const ctx = canvas.getContext('2d', { willReadFrequently: true })\n          ctx!.drawImage(img, 0, 0)\n          const step = 5\n          let red = 0,\n            green = 0,\n            blue = 0,\n            pixelsCount = 0\n          for (let y = canvas.height * 0.25; y < canvas.height / 2; y += step) {\n            for (let x = canvas.width * 0.25; x < canvas.width / 2; x += step) {\n              const data = ctx!.getImageData(x, y, 1, 1).data\n              red += data[0]\n              green += data[1]\n              blue += data[2]\n              pixelsCount++\n            }\n          }\n          const color = `rgb(${Math.round(red / pixelsCount)}, ${Math.round(green / pixelsCount)}, ${Math.round(blue / pixelsCount)})`\n          setDominantColor(color)\n        }\n      }\n    }\n  }, [url, isSuccess])\n\n  return { dominantColor, canvasRef }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/hooks/usePageSearchParams.ts",
    "content": "import { useSearchParams } from 'react-router'\nimport { useDebounceValue } from './index'\n\nexport const usePageSearchParams = () => {\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const search = searchParams.get('search') || ''\n  const [debouncedSearch] = useDebounceValue(search, 500)\n\n  const sortBy = searchParams.get('sortBy') || 'addedAt'\n  const sortDirection = (searchParams.get('sortDirection') as 'asc' | 'desc') || 'desc'\n  const tagsIds = searchParams.get('tags')?.split(',').filter(Boolean) || []\n  const artistsIds = searchParams.get('artists')?.split(',').filter(Boolean) || []\n\n  const pageNumber = Number(searchParams.get('page')) || 1\n\n  const updateSearchParams = (updates: Record<string, string | string[] | number | undefined>) => {\n    setSearchParams((prev) => {\n      Object.entries(updates).forEach(([key, value]) => {\n        if (value === undefined || value === '' || (Array.isArray(value) && value.length === 0)) {\n          prev.delete(key)\n        } else if (Array.isArray(value)) {\n          prev.set(key, value.join(','))\n        } else {\n          prev.set(key, value.toString())\n        }\n      })\n\n      // Reset page to 1 if anything other than page changed\n      if (\n        !updates.page &&\n        (updates.search !== undefined || updates.sortBy || updates.tags || updates.artists)\n      ) {\n        prev.delete('page')\n      }\n\n      return prev\n    })\n  }\n\n  const handlePageChange = (page: number) => {\n    updateSearchParams({ page: page === 1 ? undefined : page })\n  }\n\n  const handleSearchChange = (value: string) => {\n    updateSearchParams({ search: value })\n  }\n\n  const handleSortChange = (sortBy: string, sortDirection: string) => {\n    updateSearchParams({ sortBy, sortDirection })\n  }\n\n  const handleTagsChange = (tags: string[]) => {\n    updateSearchParams({ tags })\n  }\n\n  const handleArtistsChange = (artists: string[]) => {\n    updateSearchParams({ artists })\n  }\n\n  return {\n    search,\n    debouncedSearch,\n    sortBy,\n    sortDirection,\n    tagsIds,\n    artistsIds,\n    pageNumber,\n    handlePageChange,\n    handleSearchChange,\n    handleSortChange,\n    handleTagsChange,\n    handleArtistsChange,\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/AddToPlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const AddToPlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <path\n      fill=\"#000\"\n      d=\"M8.134 4.795v2.456h2.34v.776h-2.34V10.5h-.84V8.026H4.966v-.776h2.328V4.795h.84Z\"\n    />\n    <path\n      fill=\"#fff\"\n      d=\"M5.89 16.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.665 2.333 2.333 0 0 0 0-4.665ZM17.89 14.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.666 2.333 2.333 0 0 0 0-4.666ZM10.902 5.9l10.489-1.998v1l-10.5 2 .011-1.003Z\"\n    />\n    <path fill=\"#fff\" d=\"M8.39 11.5h1v8l-1-.533V11.5ZM20.39 4.964l1-.464v13l-1-.928V4.963Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/ArrowBackIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ArrowBackIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"40\"\n    height=\"40\"\n    viewBox=\"0 0 40 40\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      d=\"M33.3337 18.3332H13.0503L22.367 9.0165L20.0003 6.6665L6.66699 19.9998L20.0003 33.3332L22.3503 30.9832L13.0503 21.6665H33.3337V18.3332Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/ArrowDownIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ArrowDownIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={20}\n    height={20}\n    viewBox=\"0 0 20 20\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M6.175 7.158 10 10.975l3.825-3.817L15 8.333l-5 5-5-5 1.175-1.175Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/CheckedIcon.tsx",
    "content": "import React from 'react'\n\nexport const CheckedIcon = () => (\n  <svg width={18} height={18} viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M16 0H2C0.89 0 0 0.9 0 2V16C0 17.1 0.89 18 2 18H16C17.11 18 18 17.1 18 16V2C18 0.9 17.11 0 16 0ZM7 14L2 9L3.41 7.59L7 11.17L14.59 3.58L16 5L7 14Z\"\n      fill=\"white\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/ClockIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ClockIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    viewBox=\"0 0 28 28\"\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14 3c6.075 0 11 4.925 11 11s-4.925 11-11 11S3 20.075 3 14 7.925 3 14 3Zm0 2a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm.5 8.5H18v2h-5.5v-7h2v5Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M0 0h28v28H0z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/CreateIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const CreateIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M16 2.667C8.64 2.667 2.667 8.64 2.667 16S8.64 29.333 16 29.333 29.333 23.36 29.333 16 23.36 2.666 16 2.666Zm6.667 14.666h-5.334v5.334h-2.666v-5.334H9.333v-2.666h5.334V9.332h2.666v5.333h5.334v2.667Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/DeleteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DeleteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={10}\n    height={12}\n    viewBox=\"0 0 10 12\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M7.333 4.25v5.833H2.666V4.25h4.667ZM6.458.75H3.54l-.583.583H.916V2.5h8.167V1.333H7.04L6.458.75Zm2.041 2.333h-7v7a1.17 1.17 0 0 0 1.167 1.167h4.667a1.17 1.17 0 0 0 1.166-1.167v-7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/DeleteTagIconButton.tsx",
    "content": "import React from 'react'\n\nexport const DeleteTagIconButton = () => (\n  <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M6.66667 0C2.98 0 0 2.98 0 6.66667C0 10.3533 2.98 13.3333 6.66667 13.3333C10.3533 13.3333 13.3333 10.3533 13.3333 6.66667C13.3333 2.98 10.3533 0 6.66667 0ZM10 9.06L9.06 10L6.66667 7.60667L4.27333 10L3.33333 9.06L5.72667 6.66667L3.33333 4.27333L4.27333 3.33333L6.66667 5.72667L9.06 3.33333L10 4.27333L7.60667 6.66667L10 9.06Z\"\n      fill=\"#A9A9A9\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/DislikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DislikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 28 28\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-1.12 0-2.217.292-3.185.805L14 10.5h3.5L14 22.167l1.167-10.5h-3.5l1.796-6.289C12.215 4.212 10.512 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.818 4.854 8.376 11.667 14.583 6.382-5.763 11.667-9.637 11.667-14.583 0-3.594-2.824-6.417-6.417-6.417Zm-7.303 16.018c-4.422-3.955-7.28-6.685-7.28-9.601A4.044 4.044 0 0 1 8.75 5.833c.688 0 1.388.175 2.018.49L8.575 14h3.99l-.618 5.518Zm5.705-1.4 2.986-9.951h-3.395l.712-2.124c.42-.14.863-.21 1.295-.21a4.044 4.044 0 0 1 4.083 4.084c0 2.578-2.356 5.168-5.681 8.201Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/DownloadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DownloadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={22}\n    height={22}\n    viewBox=\"0 0 22 22\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M11.733 14.164V5.867h-1.466v8.286l-2.822-3.28-1.112.954 4.668 5.43 4.687-5.427-1.112-.958-2.843 3.292ZM11 0C4.925 0 0 4.925 0 11s4.925 11 11 11 11-4.925 11-11S17.075 0 11 0Zm0 20.533c-5.257 0-9.533-4.277-9.533-9.533 0-5.257 4.276-9.533 9.533-9.533 5.256 0 9.533 4.276 9.533 9.533 0 5.256-4.277 9.533-9.533 9.533Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/EditIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const EditIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m13.888 9.517.844.766-8.305 7.55h-.844v-.766l8.305-7.55Zm3.3-5.017a.966.966 0 0 0-.641.242l-1.678 1.525 3.438 3.125 1.677-1.525a.778.778 0 0 0 0-1.175l-2.145-1.95a.949.949 0 0 0-.65-.242Zm-3.3 2.658L3.75 16.375V19.5h3.438l10.138-9.217-3.438-3.125Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/HomeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const HomeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      d=\"M16.0001 7.58667L22.6667 13.5867V24H20.0001V16H12.0001V24H9.33341V13.5867L16.0001 7.58667ZM16.0001 4L2.66675 16H6.66675V26.6667H14.6667V18.6667H17.3334V26.6667H25.3334V16H29.3334L16.0001 4Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/ImageUploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ImageUploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={35}\n    height={34}\n    fill=\"none\"\n    viewBox=\"0 0 35 34\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M30.834 3.667v20h-20v-20h20Zm0-3.334h-20a3.343 3.343 0 0 0-3.333 3.334v20C7.5 25.5 9 27 10.834 27h20c1.833 0 3.333-1.5 3.333-3.333v-20c0-1.834-1.5-3.334-3.333-3.334ZM16.667 16.45l2.817 3.767 4.133-5.167 5.55 6.95H12.501l4.166-5.55ZM.834 7v23.333c0 1.834 1.5 3.334 3.333 3.334h23.334v-3.334H4.167V7H.834Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/KeyboardArrowLeftIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowLeftIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path fill=\"currentColor\" d=\"M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/KeyboardArrowRightIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowRightIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width={24} height={24} fill=\"none\" {...props}>\n    <path fill=\"#fff\" d=\"M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LanguageIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LanguageIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    height=\"24\"\n    fill=\"none\"\n    stroke=\"#fff\"\n    strokeWidth=\"0.75\"\n    {...props}>\n    <circle cx=\"12\" cy=\"12\" r=\"10\" />\n    <path d=\"M12,22 C14.6666667,19.5757576 16,16.2424242 16,12 C16,7.75757576 14.6666667,4.42424242 12,2 C9.33333333,4.42424242 8,7.75757576 8,12 C8,16.2424242 9.33333333,19.5757576 12,22 Z\" />\n    <path d=\"M2.5 9H21.5M2.5 15H21.5\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LibraryIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LibraryIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"  currentColor\"\n      d=\"M26.667 2.667h-16A2.674 2.674 0 0 0 8 5.332v16C8 22.8 9.2 24 10.667 24h16c1.466 0 2.666-1.2 2.666-2.667v-16c0-1.467-1.2-2.667-2.666-2.667Zm0 16.666a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12Zm-10 .667A3.335 3.335 0 0 0 20 16.666v-5.333a2 2 0 0 1 2-2h.667a1.333 1.333 0 1 0 0-2.667h-2a2 2 0 0 0-2 2v3.196c0 .882-1.119 1.471-2 1.471a3.334 3.334 0 0 0 0 6.667ZM5.333 9.333a1.333 1.333 0 1 0-2.666 0v17.333c0 1.467 1.2 2.667 2.666 2.667h17.334a1.333 1.333 0 0 0 0-2.666H7.333a2 2 0 0 1-2-2V9.332Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    viewBox=\"0 0 28 28\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-2.03 0-3.978.945-5.25 2.438C12.728 4.445 10.78 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.41 3.967 8.003 9.975 13.463L14 24.908l1.692-1.54c6.008-5.448 9.975-9.041 9.975-13.451 0-3.594-2.824-6.417-6.417-6.417Zm-5.133 18.142-.117.116-.117-.116C8.33 16.613 4.667 13.288 4.667 9.917c0-2.334 1.75-4.084 4.083-4.084 1.797 0 3.547 1.155 4.165 2.754h2.182c.606-1.599 2.356-2.754 4.153-2.754 2.333 0 4.083 1.75 4.083 4.084 0 3.371-3.663 6.696-9.216 11.725Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LikeIconFill.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIconFill = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 29 28\"\n    width={29}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14.4 6.04a6.137 6.137 0 0 1 8.655.248c2.375 2.47 2.457 6.402.247 8.967L14.4 24.5l-8.902-9.245c-2.21-2.566-2.126-6.504.248-8.967C8.123 3.823 11.927 3.74 14.4 6.04Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M.4 0h28v28H.4z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LikeInSquareIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeInSquareIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <rect width={32} height={32} fill=\"url(#a)\" rx={2} />\n    <path\n      fill=\"#fff\"\n      d=\"M16 10.158c1.645-1.597 4.186-1.544 5.77.173 1.583 1.717 1.638 4.453.165 6.237L16 23l-5.934-6.432c-1.473-1.784-1.418-4.524.165-6.237 1.585-1.715 4.121-1.773 5.77-.173Z\"\n    />\n    <defs>\n      <linearGradient id=\"a\" x1={1} x2={32} y1={1} y2={30.5} gradientUnits=\"userSpaceOnUse\">\n        <stop stopColor=\"#3822EA\" />\n        <stop offset={1} stopColor=\"#C7E9D7\" />\n      </linearGradient>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LiveWaveIcon/LiveWaveIcon.module.css",
    "content": ".bar {\n  transform-origin: center bottom;\n  animation: wave 1.2s ease-in-out infinite alternate;\n}\n\n@keyframes wave {\n  0% {\n    transform: scaleY(0.4);\n  }\n\n  50% {\n    transform: scaleY(1);\n  }\n\n  100% {\n    transform: scaleY(0.6);\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LiveWaveIcon/LiveWaveIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nimport s from './LiveWaveIcon.module.css'\n\nexport const LiveWaveIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <rect\n      x={2}\n      y={8}\n      width={2}\n      height={8}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '0ms' }}\n    />\n    <rect\n      x={6}\n      y={4}\n      width={2}\n      height={16}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '150ms' }}\n    />\n    <rect\n      x={10}\n      y={6}\n      width={2}\n      height={12}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '300ms' }}\n    />\n    <rect\n      x={14}\n      y={2}\n      width={2}\n      height={20}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '450ms' }}\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LiveWaveIcon/index.ts",
    "content": "export { LiveWaveIcon } from './LiveWaveIcon'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/LogoutIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const LogoutIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m17 8-1.41 1.41L17.17 11H9v2h8.17l-1.58 1.58L17 16l4-4-4-4ZM5 5h7V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h7v-2H5V5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/MoreIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const MoreIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={16}\n    height={4}\n    viewBox=\"0 0 16 4\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M2 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM8 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM16 2a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/PauseIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PauseIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 40 40\"\n    width={40}\n    height={40}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"#fff\"\n      d=\"M20 0c11.046 0 20 8.954 20 20s-8.954 20-20 20S0 31.046 0 20 8.954 0 20 0Zm-6 11a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Zm9 0a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/PlayIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlayIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={72}\n    height={72}\n    viewBox=\"0 0 72 72\"\n    fill=\"none\"\n    {...props}>\n    <circle cx={36} cy={36} r={36} fill=\"#FF38B6\" />\n    <path\n      fill=\"#000\"\n      d=\"M49.287 36.512c.865-.486.865-1.7 0-2.186l-19.47-10.93c-.864-.485-1.946.122-1.946 1.093v21.86c0 .971 1.082 1.579 1.947 1.093l19.469-10.93Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/PlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M28 4H4a2.675 2.675 0 0 0-2.667 2.667v18.666C1.333 26.8 2.533 28 4 28h24c1.467 0 2.667-1.2 2.667-2.667V6.667C30.667 5.2 29.467 4 28 4Zm0 21.333H4V6.667h24v18.666ZM10.667 20c0-2.213 1.786-4 4-4 .466 0 .92.093 1.333.24V8h6.667v2.667h-4v9.373a4.003 4.003 0 0 1-4 3.96c-2.214 0-4-1.787-4-4Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/PlusIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlusIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <path\n      fill=\"var(--color-text-secondary)\"\n      d=\"M30 0a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h28ZM15 9v6H9v2h6v6h2v-6h6v-2h-6V9h-2Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/ProfileIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const ProfileIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2Zm0 16h-4.83L12 20.17 9.83 18H5V4h14v14Zm-7-7c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3Zm0-4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1Zm6 8.58c0-2.5-3.97-3.58-6-3.58s-6 1.08-6 3.58V17h12v-1.42ZM8.48 15c.74-.51 2.23-1 3.52-1s2.78.49 3.52 1H8.48Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/RepeatIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const RepeatIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M9.333 9.333h13.334v4L28 8l-5.333-5.333v4h-16v8h2.666V9.332Zm13.334 13.333H9.333v-4L4 24l5.333 5.333v-4h16v-8h-2.666v5.334Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/SearchIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SearchIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 32 32\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m23.775 22.356 5.817 6.137c.56.59.541 1.534-.04 2.1a1.414 1.414 0 0 1-2.024-.04l-5.822-6.145c-1.979 1.522-4.21 2.36-6.695 2.512a11.872 11.872 0 0 1-4.822-.691c-1.556-.563-2.912-1.366-4.07-2.41-1.159-1.042-2.107-2.313-2.843-3.813a12.37 12.37 0 0 1-1.254-4.779 12.41 12.41 0 0 1 .687-4.898c.557-1.58 1.35-2.958 2.378-4.136 1.028-1.177 2.281-2.14 3.76-2.89a11.915 11.915 0 0 1 4.707-1.28c1.66-.102 3.268.129 4.823.692 1.555.563 2.912 1.366 4.07 2.409 1.159 1.043 2.106 2.314 2.843 3.814a12.368 12.368 0 0 1 1.253 4.779 12.567 12.567 0 0 1-.21 3.162 12.259 12.259 0 0 1-.958 2.929 12.892 12.892 0 0 1-1.6 2.548Zm-8.935 1.635a9.024 9.024 0 0 0 3.596-.982 9.525 9.525 0 0 0 2.869-2.216c.786-.9 1.394-1.952 1.823-3.156a9.4 9.4 0 0 0 .53-3.743 9.367 9.367 0 0 0-.963-3.65c-.566-1.143-1.292-2.113-2.178-2.91a9.443 9.443 0 0 0-3.106-1.847 8.992 8.992 0 0 0-3.685-.534 9.025 9.025 0 0 0-3.596.982A9.524 9.524 0 0 0 7.26 8.15c-.785.9-1.393 1.953-1.822 3.157a9.4 9.4 0 0 0-.53 3.742 9.367 9.367 0 0 0 .962 3.65c.567 1.144 1.293 2.114 2.179 2.91a9.443 9.443 0 0 0 3.106 1.848 8.994 8.994 0 0 0 3.685.534Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/ShuffleIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ShuffleIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M14.12 12.227 7.213 5.333l-1.88 1.88 6.893 6.894 1.894-1.88Zm5.213-6.894 2.72 2.72-16.72 16.734 1.88 1.88 16.733-16.72 2.72 2.72V5.334h-7.333Zm.44 12.547-1.88 1.88 4.173 4.173-2.733 2.734h7.333v-7.334l-2.72 2.72-4.173-4.173Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/SkipNextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipNextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"m8 24 11.333-8L8 8v16Zm2.667-10.853L14.707 16l-4.04 2.853v-5.706ZM21.333 8H24v16h-2.667V8Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/SkipPreviousIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipPreviousIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M8 8h2.667v16H8V8Zm4.667 8L24 24V8l-11.333 8Zm8.666 2.853L17.293 16l4.04-2.853v5.706Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/TextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M14.17 5 19 9.83V19H5V5h9.17Zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V9.83c0-.53-.21-1.04-.59-1.41l-4.83-4.83c-.37-.38-.88-.59-1.41-.59ZM7 15h10v2H7v-2Zm0-4h10v2H7v-2Zm0-4h7v2H7V7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/TrackIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TrackIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m16 4 .013 14.067a5.329 5.329 0 0 0-2.666-.734A5.335 5.335 0 0 0 8 22.667 5.335 5.335 0 0 0 13.347 28c2.96 0 5.32-2.387 5.32-5.333V9.333H24V4h-8Zm-2.653 21.333a2.674 2.674 0 0 1-2.667-2.666c0-1.467 1.2-2.667 2.667-2.667 1.466 0 2.666 1.2 2.666 2.667 0 1.466-1.2 2.666-2.666 2.666Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/UncheckedIcon.tsx",
    "content": "import React from 'react'\n\nexport const UncheckedIcon = () => (\n  <svg width={18} height={18} viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M16 2V16H2V2H16ZM16 0H2C0.9 0 0 0.9 0 2V16C0 17.1 0.9 18 2 18H16C17.1 18 18 17.1 18 16V2C18 0.9 17.1 0 16 0Z\"\n      fill=\"white\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/UploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const UploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M24 20v4H8v-4H5.333v4c0 1.467 1.2 2.667 2.667 2.667h16c1.467 0 2.667-1.2 2.667-2.667v-4H24ZM9.333 12l1.88 1.88 3.454-3.44v10.894h2.666V10.44l3.454 3.44 1.88-1.88L16 5.333 9.333 12Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/VolumeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M4 12v8h5.333L16 26.667V5.333L9.333 12H4Zm9.333-.227v8.454l-2.893-2.894H6.667v-2.666h3.773l2.893-2.894ZM22 16a6 6 0 0 0-3.333-5.373V21.36A5.965 5.965 0 0 0 22 16ZM18.667 4.307v2.746C22.52 8.2 25.333 11.773 25.333 16c0 4.227-2.813 7.8-6.666 8.947v2.746C24.013 26.48 28 21.707 28 16S24.013 5.52 18.667 4.307Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/VolumeMuteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeMuteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width={24} height={24} {...props}>\n    <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n    <g fill=\"currentColor\">\n      <path d=\"M16.25 13.42c.15-.45.25-.92.25-1.42A4.5 4.5 0 0 0 14 7.97v3.2l2.25 2.25z\" />\n      <path d=\"M19 12c0 1.21-.31 2.34-.85 3.32l1.46 1.46A8.973 8.973 0 0 0 21 12c0-3.83-2.4-7.11-5.78-8.4-.59-.23-1.22.23-1.22.86v.19c0 .38.25.71.61.85C17.18 6.54 19 9.06 19 12zM2.1 3.51a.996.996 0 0 0 0 1.41L6.17 9H4c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71v-2.76l3.32 3.32c-.23.13-.47.24-.71.35-.37.16-.6.52-.6.91 0 .7.7 1.2 1.35.94.5-.2.99-.45 1.44-.73l2.28 2.28a.996.996 0 1 0 1.41-1.41L3.51 3.51a.996.996 0 0 0-1.41 0zM12 9.17V6.41c0-.89-1.08-1.34-1.71-.71l-.88.89L12 9.17z\" />\n    </g>\n  </svg>\n)\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/icons/index.ts",
    "content": "export * from './AddToPlaylistIcon'\nexport * from './ArrowDownIcon'\nexport * from './ClockIcon'\nexport * from './CreateIcon'\nexport * from './DeleteIcon'\nexport * from './DislikeIcon'\nexport * from './DownloadIcon'\nexport * from './EditIcon'\nexport * from './HomeIcon'\nexport * from './ImageUploadIcon'\nexport * from './LanguageIcon'\nexport * from './KeyboardArrowLeftIcon'\nexport * from './KeyboardArrowRightIcon'\nexport * from './LibraryIcon'\nexport * from './LikeIcon'\nexport * from './LikeIconFill'\nexport * from './LikeInSquareIcon'\nexport * from './LiveWaveIcon'\nexport * from './LogoutIcon'\nexport * from './MoreIcon'\nexport * from './PauseIcon'\nexport * from './PlayIcon'\nexport * from './PlaylistIcon'\nexport * from './PlusIcon'\nexport * from './ProfileIcon'\nexport * from './RepeatIcon'\nexport * from './SearchIcon'\nexport * from './ShuffleIcon'\nexport * from './SkipNextIcon'\nexport * from './SkipPreviousIcon'\nexport * from './TextIcon'\nexport * from './ArrowBackIcon'\nexport * from './CheckedIcon'\nexport * from './UncheckedIcon'\nexport * from './DeleteTagIconButton'\nexport * from './TrackIcon'\nexport * from './UploadIcon'\nexport * from './VolumeIcon'\nexport * from './VolumeMuteIcon'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/model/ui-store.ts",
    "content": "import { create } from 'zustand'\n\ninterface UIState {\n  isCreatePlaylistModalOpen: boolean\n  isCreateTrackModalOpen: boolean\n  isAuthModalOpen: boolean\n  editingPlaylistId: string | null\n  editingTrackId: string | null\n\n  openCreatePlaylistModal: (id?: string) => void\n  closeCreatePlaylistModal: () => void\n\n  openCreateTrackModal: (id?: string) => void\n  closeCreateTrackModal: () => void\n\n  openAuthModal: () => void\n  closeAuthModal: () => void\n}\n\nexport const useUIStore = create<UIState>((set) => ({\n  isCreatePlaylistModalOpen: false,\n  isCreateTrackModalOpen: false,\n  isAuthModalOpen: false,\n  editingPlaylistId: null,\n  editingTrackId: null,\n\n  openCreatePlaylistModal: (id) =>\n    set({\n      isCreatePlaylistModalOpen: true,\n      editingPlaylistId: id || null,\n    }),\n  closeCreatePlaylistModal: () =>\n    set({\n      isCreatePlaylistModalOpen: false,\n      editingPlaylistId: null,\n    }),\n\n  openCreateTrackModal: (id) =>\n    set({\n      isCreateTrackModalOpen: true,\n      editingTrackId: id || null,\n    }),\n  closeCreateTrackModal: () =>\n    set({\n      isCreateTrackModalOpen: false,\n      editingTrackId: null,\n    }),\n\n  openAuthModal: () => set({ isAuthModalOpen: true }),\n  closeAuthModal: () => set({ isAuthModalOpen: false }),\n}))\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/translations/i18nConfiguration.ts",
    "content": "import i18n from 'i18next'\nimport LanguageDetector from 'i18next-browser-languagedetector'\nimport { initReactI18next } from 'react-i18next'\n\nimport translationEN from './languages/en.json'\nimport translationRu from './languages/ru.json'\n\nconst defaultLanguage = localStorage.getItem('locale') || 'en'\n\nconst resources = {\n  en: {\n    translation: translationEN,\n  },\n  ru: {\n    translation: translationRu,\n  },\n}\n\ni18n\n  .use(LanguageDetector)\n  .use(initReactI18next)\n  .init({\n    resources,\n    lng: defaultLanguage,\n    fallbackLng: 'en',\n    interpolation: {\n      escapeValue: false,\n    },\n  })\n\nexport default i18n\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/translations/languages/en.json",
    "content": "{\n  \"artists\": {\n    \"label\": \"Artists\",\n    \"placeholder\": \"Search by artists\"\n  },\n  \"auth\": {\n    \"button\": {\n      \"continue_without_sign_in\": \"Continue without Sign in\",\n      \"sign_in\": \"Sign in\",\n      \"sign_in_with_apihub\": \"Sign in with APIHub\",\n      \"logging_out\": \"Logging out...\"\n    },\n    \"title\": {\n      \"logout\": \"Logout\",\n      \"my_profile\": \"My Profile\"\n    },\n    \"modal\": {\n      \"title\": \"Millions of Songs. Free on Musicfun.\"\n    }\n  },\n  \"button\": {\n    \"choose\": \"Choose\",\n    \"create\": \"Create\",\n    \"edit\": \"Edit\",\n    \"delete\": \"Delete\",\n    \"cancel\": \"Cancel\",\n    \"update\": \"Update\",\n    \"edit_profile\": \"Edit profile\",\n    \"saving\": \"Saving...\",\n    \"save_changes\": \"Save Changes\"\n  },\n  \"description\": {\n    \"label\": {\n      \"description\": \"Description\"\n    },\n    \"title\": {\n      \"max_value\": \"Description must be less than {{ quantity }} characters\"\n    }\n  },\n  \"playlists\": {\n    \"button\": {\n      \"choose_playlist\": \"Choose playlist\",\n      \"create_playlist\": \"Create playlist\",\n      \"edit\": \"Edit\"\n    },\n    \"placeholder\": {\n      \"enter_playlist_description\": \"Enter playlist description\",\n      \"enter_playlist_title\": \"Enter playlist title\",\n      \"search_playlist\": \"Search playlist\"\n    },\n    \"title\": {\n      \"all_playlists\": \"All Playlists\",\n      \"create_playlist\": \"Create Playlist\",\n      \"edit_playlist\": \"Edit Playlist\",\n      \"new_playlists\": \"New playlists\",\n      \"no_playlists\": \"No playlists yet\",\n      \"playlists_not_found\": \"No playlists found\"\n    },\n    \"label\": {\n      \"load_error\": \"Unable to load the playlist\"\n    },\n    \"aria_labels\": {\n      \"open_playlist\": \"Open playlist {{title}}\"\n    }\n  },\n  \"sidebar\": {\n    \"all_playlists\": \"All Playlists\",\n    \"all_tracks\": \"All Tracks\",\n    \"create_playlist\": \"Create Playlist\",\n    \"home\": \"Home\",\n    \"upload_track\": \"Upload Track\",\n    \"your_library\": \"Your Library\"\n  },\n  \"tabs\": {\n    \"playlists\": \"Playlists\",\n    \"tracks\": \"Tracks\",\n    \"liked_playlists\": \"Liked Playlists\",\n    \"liked_tracks\": \"Liked Tracks\",\n    \"possessive_case\": \"'s\"\n  },\n  \"tags\": {\n    \"label\": \"Hashtags\",\n    \"placeholder\": \"Search by hashtags\",\n    \"add_tag_placeholder\": \"Add tag and press Enter\"\n  },\n  \"title\": {\n    \"max_value\": \"Title must be less than {{ quantity }} characters\",\n    \"min_value\": \"Title must be at least {{ quantity }} characters\",\n    \"required\": \"Title is required\",\n    \"title\": \"Title\"\n  },\n  \"tracks\": {\n    \"button\": {\n      \"add_to_playlist\": \"Add to playlist\",\n      \"all_tracks\": \"All Tracks\",\n      \"cancel\": \"Cancel\",\n      \"create\": \"Create\",\n      \"edit\": \"Edit\",\n      \"delete\": \"Delete\",\n      \"save\": \"Save\",\n      \"show_text_song\": \"Show text song\",\n      \"upload\": \"Upload\",\n      \"choose_track\": \"Choose track\",\n      \"upload_track\": \"Upload track\",\n      \"go_back\": \"Go back\",\n      \"delete_from_playlist\": \"Delete from playlist\",\n      \"publish\": \"Publish\",\n      \"draft\": \"Draft\"\n    },\n    \"label\": {\n      \"lyrics\": \"Lyrics\",\n      \"title\": \"Title\",\n      \"no_tracks\": \"No tracks\",\n      \"load_error\": \"Unable to load the track\",\n      \"audio\": \"Audio\",\n      \"artist\": \"Artist\"\n    },\n    \"placeholder\": {\n      \"lyrics\": \"Enter track lyrics\",\n      \"search_tracks\": \"Search tracks\",\n      \"title\": \"Enter track title\",\n      \"no_lyrics\": \"No lyrics available for this track\"\n    },\n    \"title\": {\n      \"all_tracks\": \"All tracks\",\n      \"create\": \"Create Track\",\n      \"edit\": \"Edit Track\",\n      \"new_tracks\": \"New tracks\",\n      \"tracks_not_found\": \"Tracks not found\"\n    },\n    \"table\": {\n      \"track\": \"Track\",\n      \"date_added\": \"Date added\",\n      \"actions\": \"Actions\",\n      \"duration\": \"Duration\"\n    },\n    \"release\": \"Release date\",\n    \"error\": {\n      \"need_select_file\": \"Need select a file\",\n      \"incorrect_audio_format\": \"Please select correct audio-format\",\n      \"file_too_large\": \"The file is too large. Max size is {{size}} MB\",\n      \"upload_cover\": \"Error upload cover\"\n    },\n    \"success\": {\n      \"upload_cover\": \"Success Upload Cover\",\n      \"uploaded_successfully\": \"Track uploaded successfully\"\n    }\n  },\n  \"sort\": {\n    \"label\": \"Sort by\",\n    \"newest_first\": \"Newest first\",\n    \"oldest_first\": \"Oldest first\",\n    \"most_liked\": \"Most liked\",\n    \"least_liked\": \"Least liked\"\n  },\n  \"placeholder\": {\n    \"upload_cover_image\": \"Upload Cover Image\",\n    \"search_and_select\": \"Search and select...\",\n    \"selected\": \"selected\",\n    \"no_options_found\": \"No options found\",\n    \"all_options_selected\": \"All options selected\",\n    \"which_playlist\": \"In which playlist is the track?\"\n  },\n  \"profile\": {\n    \"title\": {\n      \"edit_profile\": \"Edit profile\",\n      \"max_value_name\": \"Name must be less than {{ quantity }} characters\",\n      \"min_value_name\": \"Name must be at least {{ quantity }} characters\",\n      \"max_value_surname\": \"Surname must be less than {{ quantity }} characters\",\n      \"min_value_surname\": \"Surname must be at least {{ quantity }} characters\",\n      \"required_name\": \"Name is required\",\n      \"required_surname\": \"Surname is required\"\n    },\n    \"placeholder\": {\n      \"enter_profile_name\": \"Enter profile name\",\n      \"enter_profile_surname\": \"Enter profile surname\",\n      \"upload_avatar\": \"Upload Avatar\"\n    },\n    \"label\": {\n      \"name\": \"Name\",\n      \"surname\": \"Surname\",\n      \"avatar\": \"User avatar\"\n    },\n    \"stats\": {\n      \"playlists_one\": \"Playlist\",\n      \"playlists_other\": \"Playlists\",\n      \"tracks_one\": \"Track\",\n      \"tracks_other\": \"Tracks\"\n    }\n  },\n  \"date\": {\n    \"today\": \"today\",\n    \"dayAgo\": \"{{addedAt}} day ago\",\n    \"daysAgo\": \"{{addedAt}} days ago\",\n    \"monthAgo\": \"{{addedAt}} month ago\",\n    \"monthsAgo\": \"{{addedAt}} months ago\",\n    \"created\": \"Created\"\n  },\n  \"playlist\": {\n    \"made_for\": \"Made for\",\n    \"tracks_count_one\": \"{{count}} track\",\n    \"tracks_count_other\": \"{{count}} tracks\"\n  },\n  \"player\": {\n    \"unknown_artist\": \"Unknown Artist\"\n  },\n  \"common\": {\n    \"loading\": \"Loading...\",\n    \"loading_tags\": \"Loading tags...\"\n  },\n  \"player\": {\n    \"unknown_artist\": \"Unknown Artist\"\n  },\n  \"image_uploader\": {\n    \"error\": {\n      \"invalid_format\": \"Only {{formats}} files are allowed\",\n      \"file_too_large\": \"File size must be less than {{size}}MB\"\n    }\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/translations/languages/ru.json",
    "content": "{\n  \"artists\": {\n    \"label\": \"Артисты\",\n    \"placeholder\": \"Поиск по артистам\"\n  },\n  \"auth\": {\n    \"button\": {\n      \"continue_without_sign_in\": \"Продолжить без входа\",\n      \"sign_in\": \"Войти\",\n      \"sign_in_with_apihub\": \"Войти через APIHub\",\n      \"logging_out\": \"Выход...\"\n    },\n    \"title\": {\n      \"logout\": \"Выйти\",\n      \"my_profile\": \"Мой профиль\"\n    },\n    \"modal\": {\n      \"title\": \"Миллионы песен. Бесплатно на Musicfun.\"\n    }\n  },\n  \"button\": {\n    \"choose\": \"Выбрать\",\n    \"create\": \"Создать\",\n    \"edit\": \"Редактировать\",\n    \"delete\": \"Удалить\",\n    \"cancel\": \"Отмена\",\n    \"update\": \"Обновить\",\n    \"edit_profile\": \"Редактировать профиль\",\n    \"saving\": \"Сохранение...\",\n    \"save_changes\": \"Сохранить изменения\"\n  },\n  \"description\": {\n    \"label\": {\n      \"description\": \"Описание\"\n    },\n    \"title\": {\n      \"max_value\": \"Описание должно быть короче {{ quantity }} символов\"\n    }\n  },\n  \"playlists\": {\n    \"button\": {\n      \"choose_playlist\": \"Выбрать плейлист\",\n      \"create_playlist\": \"Создать плейлист\"\n    },\n    \"placeholder\": {\n      \"enter_playlist_description\": \"Введите описание плейлиста\",\n      \"enter_playlist_title\": \"Введите название плейлиста\",\n      \"search_playlist\": \"Поиск плейлиста\"\n    },\n    \"title\": {\n      \"all_playlists\": \"Все плейлисты\",\n      \"create_playlist\": \"Создать плейлист\",\n      \"edit_playlist\": \"Редактировать плейлист\",\n      \"new_playlists\": \"Новые плейлисты\",\n      \"no_playlists\": \"Здесь пока нет плейлистов\",\n      \"playlists_not_found\": \"Плейлисты не найдены\"\n    },\n    \"label\": {\n      \"load_error\": \"Не удалось загрузить плейлист\"\n    },\n    \"aria_labels\": {\n      \"open_playlist\": \"Открыть плейлист {{title}}\"\n    }\n  },\n  \"sidebar\": {\n    \"all_playlists\": \"Все плейлисты\",\n    \"all_tracks\": \"Все треки\",\n    \"create_playlist\": \"Создать плейлист\",\n    \"home\": \"Главная\",\n    \"upload_track\": \"Загрузить трек\",\n    \"your_library\": \"Ваша библиотека\"\n  },\n  \"tabs\": {\n    \"playlists\": \"Плейлисты\",\n    \"tracks\": \"Треки\",\n    \"liked_playlists\": \"Любимые плейлисты\",\n    \"liked_tracks\": \"Любимые треки\",\n    \"possessive_case\": \"\"\n  },\n  \"tags\": {\n    \"label\": \"Хэштеги\",\n    \"placeholder\": \"Поиск по хэштегам\",\n    \"add_tag_placeholder\": \"Добавьте тег и нажмите Enter\"\n  },\n  \"title\": {\n    \"max_value\": \"Название должно быть короче {{ quantity }} символов\",\n    \"min_value\": \"Название должно содержать не менее {{ quantity }} символов\",\n    \"required\": \"Название обязательно\",\n    \"title\": \"Название\"\n  },\n  \"tracks\": {\n    \"button\": {\n      \"add_to_playlist\": \"Добавить в плейлист\",\n      \"all_tracks\": \"Все треки\",\n      \"cancel\": \"Отмена\",\n      \"create\": \"Создать\",\n      \"edit\": \"Редактировать\",\n      \"delete\": \"Удалить\",\n      \"save\": \"Сохранить\",\n      \"show_text_song\": \"Показать текст песни\",\n      \"upload\": \"Загрузить\",\n      \"choose_track\": \"Выбрать трек\",\n      \"upload_track\": \"Загрузить трек\",\n      \"go_back\": \"Назад\",\n      \"delete_from_playlist\": \"Удалить из плейлиста\",\n      \"publish\": \"Опубликовать\",\n      \"draft\": \"Черновик\"\n    },\n    \"label\": {\n      \"lyrics\": \"Текст песни\",\n      \"title\": \"Название\",\n      \"no_tracks\": \"Нет треков\",\n      \"load_error\": \"Не удалось загрузить трек\",\n      \"audio\": \"Аудио\",\n      \"artist\": \"Артист\"\n    },\n    \"placeholder\": {\n      \"lyrics\": \"Введите текст песни\",\n      \"search_tracks\": \"Поиск треков\",\n      \"title\": \"Введите название трека\",\n      \"no_lyrics\": \"Для данного трека нет текста\"\n    },\n    \"title\": {\n      \"all_tracks\": \"Все треки\",\n      \"create\": \"Создать трек\",\n      \"edit\": \"Редактировать трек\",\n      \"new_tracks\": \"Новые треки\",\n      \"tracks_not_found\": \"Треки не найдены\"\n    },\n    \"table\": {\n      \"track\": \"Трек\",\n      \"date_added\": \"Дата добавления\",\n      \"actions\": \"Действия\",\n      \"duration\": \"Длительность\"\n    },\n    \"release\": \"Дата релиза\",\n    \"error\": {\n      \"need_select_file\": \"Необходимо выбрать файл\",\n      \"incorrect_audio_format\": \"Пожалуйста, выберите правильный аудио-формат\",\n      \"file_too_large\": \"Файл слишком большой. Максимальный размер {{size}} МБ\",\n      \"upload_cover\": \"Ошибка загрузки обложки\"\n    },\n    \"success\": {\n      \"upload_cover\": \"Обложка успешно загружена\",\n      \"uploaded_successfully\": \"Трек успешно загружен\"\n    }\n  },\n  \"date\": {\n    \"today\": \"сегодня\",\n    \"yesterday\": \"вчера\",\n    \"dayAgo\": \"{{addedAt}} день назад\",\n    \"fewDaysAgo\": \"{{addedAt}} дня назад\",\n    \"daysAgo\": \"{{addedAt}} дней назад\",\n    \"monthAgo\": \"{{addedAt}} месяц назад\",\n    \"fewMonthAgo\": \"{{addedAt}} месяца назад\",\n    \"monthsAgo\": \"{{addedAt}} месяцев назад\",\n    \"created\": \"Создан\"\n  },\n  \"sort\": {\n    \"label\": \"Сортировать по\",\n    \"newest_first\": \"Сначала новые\",\n    \"oldest_first\": \"Сначала старые\",\n    \"most_liked\": \"Самые популярные\",\n    \"least_liked\": \"Наименее популярные\"\n  },\n  \"placeholder\": {\n    \"upload_cover_image\": \"Загрузить обложку\",\n    \"search_and_select\": \"Найти и выбрать...\",\n    \"selected\": \"выбрано\",\n    \"no_options_found\": \"Ничего не найдено\",\n    \"all_options_selected\": \"Выбраны все\",\n    \"which_playlist\": \"В каком плейлисте находится трек?\"\n  },\n  \"profile\": {\n    \"title\": {\n      \"edit_profile\": \"Редактировать профиль\",\n      \"required_name\": \"Имя обязательно\",\n      \"required_surname\": \"Фамилия обязательна\",\n      \"max_value_name\": \"Имя должно быть меньше {{ quantity }} символов\",\n      \"min_value_name\": \"Имя должно содержать не менее {{ quantity }} символов\",\n      \"max_value_surname\": \"Фамилия должна быть меньше {{ quantity }} символов\",\n      \"min_value_surname\": \"Фамилия должна содержать не менее {{ quantity }} символов\"\n    },\n    \"placeholder\": {\n      \"enter_profile_name\": \"Введите имя профиля\",\n      \"enter_profile_surname\": \"Введите фамилию профиля\",\n      \"upload_avatar\": \"Загрузить аватар\"\n    },\n    \"label\": {\n      \"name\": \"Имя\",\n      \"surname\": \"Фамилия\",\n      \"avatar\": \"Аватар пользователя\"\n    },\n    \"stats\": {\n      \"playlists_one\": \"Плейлист\",\n      \"playlists_few\": \"Плейлиста\",\n      \"playlists_many\": \"Плейлистов\",\n      \"tracks_one\": \"Трек\",\n      \"tracks_few\": \"Трека\",\n      \"tracks_many\": \"Треков\"\n    }\n  },\n  \"playlist\": {\n    \"made_for\": \"Сделан для\",\n    \"tracks_count_one\": \"{{count}} трек\",\n    \"tracks_count_few\": \"{{count}} трека\",\n    \"tracks_count_many\": \"{{count}} треков\",\n    \"tracks_count_other\": \"{{count}} треков\"\n  },\n  \"player\": {\n    \"unknown_artist\": \"Неизвестный исполнитель\"\n  },\n  \"common\": {\n    \"loading\": \"Загрузка...\",\n    \"loading_tags\": \"Загрузка тегов...\"\n  },\n  \"player\": {\n    \"unknown_artist\": \"Неизвестный исполнитель\"\n  },\n  \"image_uploader\": {\n    \"error\": {\n      \"invalid_format\": \"Разрешены только файлы {{formats}}\",\n      \"file_too_large\": \"Размер файла должен быть меньше {{size}}МБ\"\n    }\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/types/api-track.types.ts",
    "content": "// Type for API track (union of different formats)\nimport type { components } from '@/shared/api/schema.ts'\n\nexport type ApiTrack =\n  | components['schemas']['TrackListItemResource']\n  | components['schemas']['TrackDetailsResource']\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/types/strict.tsx",
    "content": "export type Strict<T, U extends T> = U & Record<Exclude<keyof U, keyof T>, never>\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/ui/prerender-ready.tsx",
    "content": "import { useIsFetching } from '@tanstack/react-query'\nimport { useEffect, useState } from 'react'\n\nexport function PrerenderReady() {\n  const isFetching = useIsFetching()\n  const [ready, setReady] = useState(false)\n\n  useEffect(() => {\n    // Когда все запросы ушли в 0, ставим флаг\n    if (!isFetching) {\n      setReady(true)\n    }\n  }, [isFetching])\n\n  // Рендерим только один раз, когда ready=true\n  return ready ? <div id=\"renderer_rendered\" style={{ display: 'none' }} /> : null\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/ui/utils/query-error-handler-for-rhf-factory.ts",
    "content": "import type { FieldValues, Path, UseFormSetError } from 'react-hook-form'\nimport { toast } from 'react-toastify'\n\nimport {\n  isJsonApiErrorDocument,\n  type JsonApiErrorDocument,\n  parseJsonApiErrors,\n} from '../../api/utils/json-api-error.ts'\n\nexport const queryErrorHandlerForRHFFactory = <T extends FieldValues>({\n  setError,\n}: {\n  setError?: UseFormSetError<T>\n}) => {\n  return (err: JsonApiErrorDocument) => {\n    // 400 от сервера в JSON:API формате\n    if (isJsonApiErrorDocument(err)) {\n      const { fieldErrors, globalErrors } = parseJsonApiErrors(err)\n\n      // полевые ошибки\n      for (const [field, message] of Object.entries(fieldErrors)) {\n        setError?.(field as Path<T>, { type: 'server', message })\n      }\n\n      // «глобальные» (без pointer)\n      if (globalErrors.length > 0) {\n        setError?.('root.server', {\n          type: 'server',\n          message: globalErrors.join('\\n'),\n        })\n        toast(globalErrors.join('\\n'))\n      }\n\n      return\n    }\n  }\n}\n\nexport const mutationGlobalErrorHandler = (\n  error: Error,\n  _: unknown,\n  __: unknown,\n  mutation: unknown\n) => {\n  // 400 от сервера в JSON:API формате\n  // @ts-expect-error ignore typing\n  const globalFlag = (mutation.meta as MutationMeta)?.globalErrorHandler\n  // если в meta сказали \"off\" — ничего не делаем\n  if (globalFlag === 'off') {\n    return\n  }\n\n  if (isJsonApiErrorDocument(error)) {\n    const { globalErrors } = parseJsonApiErrors(error)\n\n    // «глобальные» (без pointer)\n    if (globalErrors.length > 0) {\n      toast(globalErrors.join('\\n'))\n    }\n  }\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/authStorage.ts",
    "content": "interface TokenData {\n  accessToken: string\n}\n\ninterface RefreshTokenData {\n  refreshToken: string\n}\n\ninterface AuthStorage {\n  saveAccessToken: (accessToken: string) => void\n  getAccessToken: () => string | null\n  clearAccessToken: () => void\n  clearTokens: () => void\n  saveRefreshToken: (refreshToken: string) => void\n  getRefreshToken: () => string | null\n  clearRefreshToken: () => void\n}\n\nexport const authStorage: AuthStorage = {\n  saveAccessToken(accessToken: string) {\n    localStorage.setItem(localStorageKeys.accessToken, JSON.stringify({ accessToken }))\n  },\n  getAccessToken() {\n    const tokenString = localStorage.getItem(localStorageKeys.accessToken)\n    if (tokenString) {\n      const token = JSON.parse(tokenString) as TokenData\n      return token.accessToken\n    }\n    return null\n  },\n  clearAccessToken() {\n    localStorage.removeItem(localStorageKeys.accessToken)\n  },\n\n  clearTokens() {\n    this.clearAccessToken()\n    this.clearRefreshToken()\n  },\n  saveRefreshToken(refreshToken: string) {\n    localStorage.setItem(localStorageKeys.refreshToken, JSON.stringify({ refreshToken }))\n  },\n  getRefreshToken() {\n    const tokenString = localStorage.getItem(localStorageKeys.refreshToken)\n    if (tokenString) {\n      const token = JSON.parse(tokenString) as RefreshTokenData\n      return token.refreshToken\n    }\n    return null\n  },\n  clearRefreshToken() {\n    localStorage.removeItem(localStorageKeys.refreshToken)\n  },\n}\n\nconst localStorageKeys = {\n  refreshToken: 'tanstack-query-musicfun-refresh-token',\n  accessToken: 'tanstack-query-musicfun-access-token',\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/decode-file-from-base-64.ts",
    "content": "export const decodeFileFromBase64 = (data: string | null) => {\n  if (!data) return null\n\n  const mimeType = data.split(';')[0].split(':')[1]\n  const base64Url = data.split(',')[1]\n\n  const binaryString = atob(base64Url)\n  const binaryLength = binaryString.length\n  const bytes = new Uint8Array(binaryLength)\n\n  for (let i = 0; i < binaryLength; i++) {\n    bytes[i] = binaryString.charCodeAt(i)\n  }\n\n  const blob = new Blob([bytes], { type: mimeType })\n  return URL.createObjectURL(blob)\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/format-created-date.ts",
    "content": "import { getPluralKey } from '@/shared/utils/get-plural-key.ts'\n\nimport i18n from '../translations/i18nConfiguration.ts'\n\nexport const formatCreatedDate = (addedAt: string | undefined) => {\n  const lang = i18n.language || 'en'\n  if (!addedAt) {\n    return i18n.t('date.created')\n  }\n\n  const date = new Date(addedAt.toString())\n  const now = new Date()\n  const differTime = now.getTime() - date.getTime()\n  const differDays = Math.floor(differTime / (1000 * 60 * 60 * 24))\n  if (differDays === 0) {\n    return `${i18n.t('date.created')} ${i18n.t('date.today')}`\n  }\n  if (differDays < 30) {\n    const key = getPluralKey(differDays, lang, 'day')\n    const daysText = i18n.t(key, { addedAt: differDays })\n    return `${i18n.t('date.created')} ${daysText}`\n  }\n\n  const differMonths = Math.floor(differDays / 30)\n  if (differMonths < 12) {\n    const key = getPluralKey(differMonths, lang, 'month')\n    const monthsText = i18n.t(key, { addedAt: differMonths })\n    return `${i18n.t('date.created')} ${monthsText}`\n  }\n\n  const dateText = new Intl.DateTimeFormat(lang === 'ru' ? 'ru-RU' : 'en-GB', {\n    day: '2-digit',\n    month: '2-digit',\n    year: 'numeric',\n  }).format(date)\n\n  return `${i18n.t('date.created')} ${dateText}`\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-artist-id.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\n/**\n * Gets artist ID from relationships (if available)\n */\nexport const getArtistId = (\n  track:\n    | components['schemas']['TrackListItemResource']\n    | components['schemas']['TrackDetailsResource']\n): string | undefined => {\n  // TrackListItemResource has relationships\n  if ('relationships' in track && track.relationships?.artists?.data?.[0]?.id) {\n    return track.relationships.artists.data[0].id\n  }\n\n  // TrackDetailsResource has artists in attributes\n  if ('attributes' in track && 'artists' in track.attributes && track.attributes.artists?.[0]?.id) {\n    return track.attributes.artists[0].id\n  }\n\n  return undefined\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-artist-name.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\n/**\n * Gets artist name from different sources\n */\nexport const getArtistName = (\n  attributes:\n    | components['schemas']['TrackListItemAttributes']\n    | components['schemas']['TrackDetailsAttributes'],\n  user?: components['schemas']['UserRef']\n): string => {\n  // TrackDetailsAttributes has artists array\n  if ('artists' in attributes && attributes.artists && attributes.artists.length > 0) {\n    return attributes.artists.map((a) => a.name).join(', ')\n  }\n\n  // Otherwise use user name\n  return user?.name || 'Unknown Artist'\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-artists-by-track.ts",
    "content": "import type { SchemaIncludedArtistOutput, SchemaTrackListItemResource } from '@/shared/api/schema'\n\nexport function getArtistsByTrack(\n  track: SchemaTrackListItemResource,\n  included: SchemaIncludedArtistOutput[]\n): string {\n  const artistIds = track.relationships.artists.data.map((a) => a.id)\n  return included\n    .filter((artist) => artistIds.includes(artist.id))\n    .map((artist) => artist.attributes.name)\n    .join(', ')\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-audio-url.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\n/**\n * Extracts audio URL from track attachments\n */\nexport const getAudioUrl = (\n  attachments: components['schemas']['TrackAttachment'][] | undefined\n): string => {\n  if (!attachments || attachments.length === 0) return ''\n\n  // Search for audio file in attachments\n  const audioAttachment = attachments.find(\n    (att) =>\n      att.contentType?.startsWith('audio/') ||\n      att.originalName?.toLowerCase().match(/\\.(mp3|wav|ogg|m4a)$/)\n  )\n\n  return audioAttachment?.url || ''\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-cover-url.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\nimport { getImageByType } from '@/shared/utils/get-image-by-type.ts'\n\n/**\n * Gets track cover image\n */\nexport const getCoverUrl = (\n  images:\n    | components['schemas']['TrackImages']\n    | components['schemas']['PlaylistImagesOutputDTO']\n    | undefined\n): string => {\n  // Try to get medium-sized image\n  const mediumImage = getImageByType(images, 'medium')\n  if (mediumImage) return mediumImage.url\n\n  // If no medium, use original\n  const originalImage = getImageByType(images, 'original')\n  if (originalImage) return originalImage.url\n\n  // If no original, use first available\n  const firstImage = getImageByType(images, '')\n  return firstImage?.url || ''\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-image-by-type.ts",
    "content": "import type { components } from '@/shared/api/schema.ts'\n\n/**\n * Gets image of specific type from images array\n */\nexport const getImageByType = (\n  images:\n    | components['schemas']['TrackImages']\n    | components['schemas']['PlaylistImagesOutputDTO']\n    | undefined,\n  preferredType: string\n): components['schemas']['ImageVariant'] | undefined => {\n  if (!images?.main) return undefined\n\n  // Search image by type\n  const imageByType = images.main.find((img) => img.type === preferredType)\n  if (imageByType) return imageByType\n\n  // If not found, return first available\n  return images.main[0]\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-plural-key.ts",
    "content": "import { getRussianPluralForm } from '@/shared/utils/get-russian-plural-form.ts'\n\nexport const getPluralKey = (count: number, lang: string, type: 'day' | 'month') => {\n  if (lang === 'en') {\n    return count === 1 ? `date.${type}Ago` : `date.${type}sAgo`\n  }\n\n  if (lang === 'ru') {\n    return getRussianPluralForm(count, type)\n  }\n  return `date.${type}sAgo`\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-russian-plural-form.ts",
    "content": "export const getRussianPluralForm = (count: number, type: 'day' | 'month'): string => {\n  const lastDigit = count % 10\n\n  if (lastDigit === 1 && count !== 11) {\n    return `date.${type}Ago`\n  }\n\n  if (lastDigit >= 2 && lastDigit <= 4 && !(count >= 12 && count <= 14)) {\n    return type === 'day' ? 'date.fewDaysAgo' : 'date.fewMonthAgo'\n  }\n\n  return `date.${type}sAgo`\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/get-user-initials.ts",
    "content": "type FullName = {\n  name?: string\n  surname?: string\n}\n\nexport const getUserInitials = (fullName?: FullName, userLogin?: string) => {\n  const name = fullName?.name?.trim()\n  const surname = fullName?.surname?.trim()\n\n  if (name || surname) {\n    const initials = `${name?.[0] || ''}${surname?.[0] || ''}`.trim()\n    if (initials) return initials.toUpperCase()\n  }\n\n  if (userLogin?.trim()) {\n    const loginParts = userLogin.trim().split(/\\s+/)\n    if (loginParts.length >= 2) {\n      return `${loginParts[0][0]}${loginParts[1][0]}`.toUpperCase()\n    }\n    return userLogin.slice(0, 2).toUpperCase()\n  }\n\n  return 'U'\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/index.ts",
    "content": "import * as ValidationUtils from './validators'\n\nexport { ValidationUtils as VU }\nexport { decodeFileFromBase64 } from './decode-file-from-base-64'\nexport { formatCreatedDate } from './format-created-date'\nexport { getArtistsByTrack } from './get-artists-by-track'\nexport { setLocale } from './set-locale'\nexport type { Locale } from './set-locale'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/join-url.test.ts",
    "content": "// joinUrl.test.ts\nimport { describe, expect, it } from 'vitest'\n\nimport { joinUrl } from './join-url'\n\ndescribe('joinUrl', () => {\n  it('joins simple segments', () => {\n    expect(joinUrl('api', 'users')).toBe('api/users')\n  })\n\n  it('keeps protocol and host correctly', () => {\n    expect(joinUrl('https://example.com', 'api', 'v1', 'users')).toBe(\n      'https://example.com/api/v1/users'\n    )\n  })\n\n  it('trims trailing slashes on first segment', () => {\n    expect(joinUrl('https://example.com///', 'api', 'v1')).toBe('https://example.com/api/v1')\n  })\n\n  it('trims leading slashes on last segment', () => {\n    expect(joinUrl('https://example.com', '/api', '/v1///')).toBe('https://example.com/api/v1')\n  })\n\n  it('trims both sides on middle segments', () => {\n    expect(joinUrl('https://example.com/', '/api/', '/v1/', '/users/')).toBe(\n      'https://example.com/api/v1/users'\n    )\n  })\n\n  it('handles numeric segments', () => {\n    expect(joinUrl('/api', 'users', 123)).toBe('/api/users/123')\n  })\n\n  it('replaces null and undefined with marker', () => {\n    expect(joinUrl('/api', null, undefined, 'users')).toBe(\n      '/api/__CHECK_SEGMENT_VALUE__/__CHECK_SEGMENT_VALUE__/users'\n    )\n  })\n\n  it('replaces empty strings with marker', () => {\n    expect(joinUrl('', '/api/', '', 'users/', '')).toBe(\n      '__CHECK_SEGMENT_VALUE__/api/__CHECK_SEGMENT_VALUE__/users/__CHECK_SEGMENT_VALUE__'\n    )\n  })\n\n  it('returns empty string for empty input', () => {\n    expect(joinUrl()).toBe('')\n  })\n\n  it('works with root slash as first segment', () => {\n    expect(joinUrl('/', 'api', 'users')).toBe('/api/users')\n  })\n\n  it('does not duplicate slashes after protocol with path having leading slash', () => {\n    expect(joinUrl('https://example.com/', '/api', '/users')).toBe('https://example.com/api/users')\n  })\n})\n\n// Есть и другие варианты организации тестов.\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/join-url.ts",
    "content": "export function joinUrl(...parts: Array<string | number | null | undefined>): string {\n  const segments = parts.map((p) => {\n    // Replace null, undefined, or empty string with marker\n    if (p === null || p === undefined || String(p).trim().length === 0) {\n      return '__CHECK_SEGMENT_VALUE__'\n    }\n    return String(p)\n  })\n\n  if (segments.length === 0) return ''\n\n  const cleaned = segments.map((segment, index) => {\n    if (index === 0) {\n      // first: trim only trailing slashes\n      return segment.replace(/\\/+$/g, '')\n    }\n    if (index === segments.length - 1) {\n      // last: trim both leading and trailing slashes\n      return segment.replace(/^\\/+|\\/+$/g, '')\n    }\n    // middle: trim both sides\n    return segment.replace(/^\\/+|\\/+$/g, '')\n  })\n\n  return cleaned.join('/')\n}\n\n// Примеры:\n// joinUrl('https://example.com/', '/api/', '/v1/', 'users/') -> 'https://example.com/api/v1/users'\n// joinUrl('/api', 'users', 123) -> '/api/users/123'\n// joinUrl('/api', null, undefined, 'users') -> '/api/__CHECK_SEGMENT_VALUE__/__CHECK_SEGMENT_VALUE__/users'\n// joinUrl('', '/api/', '', 'users/') -> '__CHECK_SEGMENT_VALUE__/api/__CHECK_SEGMENT_VALUE__/users'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/set-locale.ts",
    "content": "import i18n from 'i18next'\n\nexport type Locale = 'en' | 'ru'\n\nconst LOCALE_KEY = 'locale'\n\n/**\n * Switches application locale using i18next and persists it to localStorage.\n * Note: language change is async inside i18next, this function does not await it.\n *\n * @param {Locale} lng - Target locale code (e.g. \"en\" or \"ru\").\n * @returns {void}\n */\nexport const setLocale = (lng: Locale): void => {\n  void i18n.changeLanguage(lng)\n  localStorage.setItem(LOCALE_KEY, lng)\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/getType.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getType(object: any) {\n  return Object.prototype.toString.call(object)\n}\n\nexport default getType\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/inNun.ts",
    "content": "function isNaN(value: number) {\n  return Number.isNaN(value)\n}\n\nexport default isNaN\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/index.ts",
    "content": "export { default as getType } from './getType'\nexport { default as inNun } from './inNun'\nexport { default as isArray } from './isArray'\nexport { default as isFunction } from './isFunction'\nexport { isNotEmptyArray } from './isNotEmptyArray'\nexport { default as isNull } from './isNull'\nexport { default as isNumber } from './isNumber'\nexport { default as isObject } from './isObject'\nexport { default as isString } from './isString'\nexport { default as isUndefined } from './isUndefined'\nexport { default as isValid } from './isValid'\nexport { default as isValidNumber } from './isValidNumber'\nexport { default as isValidObject } from './isValidObject'\nexport { default as isValidString } from './isValidString'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isArray.ts",
    "content": "import getType from './getType'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isArray<T>(value: any): value is T[] {\n  return getType(value) === '[object Array]'\n}\n\nexport default isArray\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isFunction.ts",
    "content": "import getType from './getType'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isFunction<TFunction extends (...args: any[]) => any>(value: any): value is TFunction {\n  return (\n    getType(value) === '[object AsyncFunction]' ||\n    getType(value) === '[object Function]' ||\n    getType(value) === '[object GeneratorFunction]'\n  )\n}\n\nexport default isFunction\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isNotEmptyArray.ts",
    "content": "export function isNotEmptyArray(array?: unknown[]): array is unknown[] {\n  return !!array && array.length > 0\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isNull.ts",
    "content": "function isNull(value: unknown): value is null {\n  return value === null\n}\n\nexport default isNull\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isNumber.ts",
    "content": "import getType from './getType'\n\nfunction isNumber<T extends number>(value: unknown): value is T {\n  return getType(value) === '[object Number]'\n}\n\nexport default isNumber\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isObject.ts",
    "content": "import isArray from './isArray'\nimport isNull from './isNull'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isObject<T extends Record<string, any>>(value: any): value is T {\n  return typeof value === 'object' && !isArray(value) && !isNull(value)\n}\n\nexport default isObject\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isString.ts",
    "content": "import getType from './getType'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isString<TString extends string>(value: any): value is TString {\n  return getType(value) === '[object String]'\n}\n\nexport default isString\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isUndefined.ts",
    "content": "import getType from './getType'\n\nfunction isUndefined(value: unknown): value is undefined {\n  return getType(value) === '[object Undefined]'\n}\n\nexport default isUndefined\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isValid.ts",
    "content": "import isNull from './isNull'\nimport isUndefined from './isUndefined'\n\nfunction isValid<T>(value: T | null | undefined): value is T {\n  return !isNull(value) && !isUndefined(value)\n}\n\nexport default isValid\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isValidNumber.ts",
    "content": "import isNaN from './inNun'\nimport isNumber from './isNumber'\n\nfunction isValidNumber<T extends number>(value: T): boolean\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isValidNumber(value: any): value is number\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isValidNumber(value: any) {\n  return isNumber(value) && !isNaN(value)\n}\n\nexport default isValidNumber\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isValidObject.ts",
    "content": "import { isNotEmptyArray } from './isNotEmptyArray.ts'\nimport isValid from './isValid'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isValidObject<T extends Record<string, any>>(value: T) {\n  return isValid(value) && isNotEmptyArray(Object.keys(value))\n}\n\nexport default isValidObject\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/shared/utils/validators/isValidString.ts",
    "content": "import isString from './isString'\n\nfunction isValidString<T extends string>(value: T): boolean\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isValidString<T extends string>(value: any): value is T\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isValidString(value: any) {\n  return isString(value) && value.trim().length > 0\n}\n\nexport default isValidString\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/widgets/Player/Player.module.css",
    "content": ".player {\n  grid-area: player;\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/widgets/Player/Player.tsx",
    "content": "import { useState } from 'react'\n\nimport { AudioPlayer } from '@/shared/components'\n\nimport s from './Player.module.css'\n\nexport const Player = () => {\n  const [isShuffle, setIsShuffle] = useState(false)\n  const [isRepeat, setIsRepeat] = useState(false)\n\n  return (\n    <AudioPlayer\n      onNext={() => {}}\n      onPrevious={() => {}}\n      isShuffle={isShuffle}\n      isRepeat={isRepeat}\n      onShuffle={() => setIsShuffle(!isShuffle)}\n      onRepeat={() => setIsRepeat(!isRepeat)}\n      className={s.player}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/src/widgets/Player/index.ts",
    "content": "export * from './Player'\n"
  },
  {
    "path": "apps/tanstack-query-zustand/stylelint.config.js",
    "content": "export default {\n  extends: ['stylelint-config-standard', 'stylelint-config-clean-order'],\n  rules: {\n    // Class selector pattern (allow camelCase for CSS modules)\n    'selector-class-pattern': null,\n\n    // Allow unknown at-rules (for CSS modules :global, :local etc)\n    'at-rule-no-unknown': [\n      true,\n      {\n        ignoreAtRules: ['global', 'local'],\n      },\n    ],\n  },\n\n  // File patterns to lint\n  ignoreFiles: ['dist/**/*', 'build/**/*', 'node_modules/**/*'],\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"types\": [\"vite/client\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\", // or \"NodeNext\"\n    \"moduleResolution\": \"Bundler\", // or \"NodeNext\",\n    \"noUncheckedIndexedAccess\": true\n  },\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "apps/tanstack-query-zustand/vite.config.ts",
    "content": "import path from 'node:path'\n\nimport react from '@vitejs/plugin-react'\nimport { defineConfig, loadEnv } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig(({ mode }) => {\n  const env = loadEnv(mode, process.cwd())\n\n  // Доступ к любой переменной\n  const VITE_APP_BASE_URL = env.VITE_APP_BASE_URL\n\n  console.log('✅ mode: ' + mode)\n  console.log('✅ VITE_APP_BASE_URL: ' + VITE_APP_BASE_URL)\n\n  return {\n    plugins: [react()],\n    base: '/' + VITE_APP_BASE_URL,\n    resolve: {\n      alias: {\n        '@': path.resolve(__dirname, 'src'),\n      },\n    },\n    server: {\n      host: true, // ← or '0.0.0.0'\n      port: 5174,\n      strictPort: true,\n      allowedHosts: [\n        'domain.prod', // <-- your custom host\n        'localhost', // (optional) keep localhost too\n      ],\n    },\n  }\n})\n"
  },
  {
    "path": "apps/ui-vanilla/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.cursor\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n*storybook.log\nstorybook-static\n"
  },
  {
    "path": "apps/ui-vanilla/.storybook/main.ts",
    "content": "import type { StorybookConfig } from '@storybook/react-vite'\n\nconst config: StorybookConfig = {\n  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],\n  addons: [],\n  framework: {\n    name: '@storybook/react-vite',\n    options: {},\n  },\n}\nexport default config\n"
  },
  {
    "path": "apps/ui-vanilla/.storybook/preview.tsx",
    "content": "import '../src/styles/fonts.css'\nimport '../src/styles/variables.css'\nimport '../src/styles/reset.css'\nimport '../src/styles/global.css'\n\nimport type { Preview } from '@storybook/react-vite'\nimport React from 'react'\nimport { BrowserRouter } from 'react-router'\n\nconst preview: Preview = {\n  parameters: {\n    controls: {\n      matchers: {\n        color: /(background|color)$/i,\n        date: /Date$/i,\n      },\n    },\n  },\n  decorators: [\n    (Story) => (\n      <BrowserRouter>\n        <Story />\n      </BrowserRouter>\n    ),\n  ],\n}\n\nexport default preview\n"
  },
  {
    "path": "apps/ui-vanilla/README.md",
    "content": "UI for Musicfun app without libs\n\nTODO:\n[] Add common components for TrackOverview and PlaylistOverview\n[] Refactor DropdownMenu\n"
  },
  {
    "path": "apps/ui-vanilla/eslint.config.js",
    "content": "// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format\nimport js from '@eslint/js'\nimport prettier from 'eslint-config-prettier'\nimport eslintPluginPrettier from 'eslint-plugin-prettier'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport simpleImportSort from 'eslint-plugin-simple-import-sort'\nimport storybook from 'eslint-plugin-storybook'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config(\n  { ignores: ['dist'] },\n  {\n    extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],\n    files: ['**/*.{ts,tsx}'],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n    plugins: {\n      'react-hooks': reactHooks,\n      'react-refresh': reactRefresh,\n      prettier: eslintPluginPrettier,\n      'simple-import-sort': simpleImportSort,\n    },\n    rules: {\n      ...reactHooks.configs.recommended.rules,\n      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],\n      'prettier/prettier': 'warn',\n      'simple-import-sort/imports': 'error',\n      'simple-import-sort/exports': 'error',\n    },\n  },\n  storybook.configs['flat/recommended']\n)\n"
  },
  {
    "path": "apps/ui-vanilla/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Musicfun</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/ui-vanilla/package.json",
    "content": "{\n  \"name\": \"musicfun-ui-vanilla\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"lint:css\": \"stylelint \\\"src/**/*.{css,scss}\\\"\",\n    \"lint:css:fix\": \"stylelint \\\"src/**/*.{css,scss}\\\" --fix\",\n    \"format\": \"prettier --write .\",\n    \"preview\": \"vite preview\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"build-storybook\": \"storybook build\"\n  },\n  \"dependencies\": {\n    \"react\": \"19.1.0\",\n    \"react-dom\": \"19.1.0\",\n    \"react-router\": \"7.6.2\"\n  },\n  \"devDependencies\": {\n    \"clsx\": \"^2.1.1\",\n    \"@eslint/js\": \"^9.25.0\",\n    \"@storybook/react-vite\": \"9.0.8\",\n    \"@types/node\": \"^24.0.1\",\n    \"@types/react\": \"^19.1.2\",\n    \"@types/react-dom\": \"^19.1.2\",\n    \"@vitejs/plugin-react\": \"^4.4.1\",\n    \"eslint\": \"^9.25.0\",\n    \"eslint-config-prettier\": \"^10.1.5\",\n    \"eslint-plugin-prettier\": \"^5.4.1\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.19\",\n    \"eslint-plugin-simple-import-sort\": \"^12.1.1\",\n    \"eslint-plugin-storybook\": \"9.0.8\",\n    \"globals\": \"^16.0.0\",\n    \"prettier\": \"^3.5.3\",\n    \"storybook\": \"9.0.8\",\n    \"stylelint\": \"^16.20.0\",\n    \"stylelint-config-clean-order\": \"^7.0.0\",\n    \"stylelint-config-standard\": \"^38.0.0\",\n    \"typescript\": \"~5.8.3\",\n    \"typescript-eslint\": \"^8.30.1\",\n    \"vite\": \"^6.3.5\"\n  }\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/app/App.tsx",
    "content": "import { Routing } from './routing'\n\nexport const App = () => {\n  return (\n    <>\n      <Routing />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/app/routing/Routing.tsx",
    "content": "import { Route, Routes } from 'react-router'\n\nimport { Layout } from '@/layout'\nimport { MainPage, PlaylistPage, PlaylistsPage, TrackPage, TracksPage, UserPage } from '@/pages'\n\nexport const Routing = () => (\n  <Routes>\n    <Route path=\"/\" element={<Layout />}>\n      <Route index element={<MainPage />} />\n\n      <Route path=\"/tracks\" element={<TracksPage />} />\n      <Route path=\"/tracks/:id\" element={<TrackPage />} />\n\n      <Route path=\"/playlists\" element={<PlaylistsPage />} />\n      <Route path=\"/playlists/:id\" element={<PlaylistPage />} />\n\n      <Route path=\"/user/:id\" element={<UserPage />} />\n    </Route>\n  </Routes>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/app/routing/index.ts",
    "content": "export { Routing } from './Routing'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/artists/api/artists-api.ts",
    "content": "export const MOCK_ARTISTS = [\n  {\n    id: '1',\n    name: 'Kanye West',\n    image: 'https://unsplash.it/148/148',\n  },\n  {\n    id: '2',\n    name: 'Drake & The Weeknd & Kanye West',\n    image: 'https://unsplash.it/149/149',\n  },\n  {\n    id: '3',\n    name: 'Frank Ocean',\n    image: 'https://unsplash.it/150/150',\n  },\n  {\n    id: '4',\n    name: 'Headlund',\n    image: 'https://unsplash.it/151/151',\n  },\n  {\n    id: '5',\n    name: 'Rihanna',\n    image: 'https://unsplash.it/152/152',\n  },\n  {\n    id: '6',\n    name: 'Lamar',\n    image: 'https://unsplash.it/153/153',\n  },\n  {\n    id: '7',\n    name: 'The Weeknd',\n    image: 'https://unsplash.it/154/154',\n  },\n  {\n    id: '8',\n    name: 'Kendrick Lamar',\n    image: 'https://unsplash.it/155/155',\n  },\n  {\n    id: '9',\n    name: 'J. Cole',\n    image: 'https://unsplash.it/156/156',\n  },\n  {\n    id: '10',\n    name: 'Lil Uzi Vert',\n    image: 'https://unsplash.it/157/157',\n  },\n]\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/artists/api/index.ts",
    "content": ""
  },
  {
    "path": "apps/ui-vanilla/src/features/artists/index.ts",
    "content": ""
  },
  {
    "path": "apps/ui-vanilla/src/features/artists/ui/ArtistCard/ArtistCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n\n  width: 148px;\n  height: 180px;\n}\n\n.image {\n  overflow: hidden;\n\n  width: 148px;\n  height: 148px;\n  border-radius: 50%;\n\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-align: center;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/artists/ui/ArtistCard/ArtistCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ArtistCard } from './ArtistCard'\n\nconst meta: Meta<typeof ArtistCard> = {\n  title: 'entities/ArtistCard',\n  component: ArtistCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof ArtistCard>\n\nexport const Default: Story = {\n  args: {\n    image: 'https://unsplash.it/182/182',\n    name: 'Kanye West',\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    image: 'https://unsplash.it/183/183',\n    name: 'Drake & The Weeknd & Kanye West',\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/artists/ui/ArtistCard/ArtistCard.tsx",
    "content": "import { Typography } from '@/shared/components'\n\nimport s from './ArtistCard.module.css'\n\ntype Props = {\n  image: string\n  name: string\n}\n\nexport const ArtistCard = ({ image, name }: Props) => {\n  return (\n    <div className={s.card}>\n      <div className={s.image}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <Typography variant=\"h3\" className={s.title}>\n        {name}\n      </Typography>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/artists/ui/ArtistCard/index.ts",
    "content": "export * from './ArtistCard'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/index.ts",
    "content": "export * from './ui'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/ui/LoginButtonAndModal/LoginButtonAndModal.module.css",
    "content": ".dialog {\n  width: 376px;\n  padding-bottom: 22px;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n  align-items: center;\n\n  text-align: center;\n}\n\n.icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 60px;\n  height: 60px;\n  border-radius: 50%;\n\n  font-size: 24px;\n\n  background-color: var(--color-accent);\n}\n\n.button {\n  height: 55px;\n}\n\n.secondary {\n  background-color: #555;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/ui/LoginButtonAndModal/LoginButtonAndModal.tsx",
    "content": "import clsx from 'clsx'\nimport { useState } from 'react'\n\nimport { Button } from '@/shared/components/Button'\nimport { Dialog, DialogContent, DialogHeader } from '@/shared/components/Dialog'\nimport { Typography } from '@/shared/components/Typography'\n\nimport s from './LoginButtonAndModal.module.css'\n\nexport const LoginButtonAndModal = () => {\n  const [isOpen, setIsOpen] = useState(false)\n\n  const handleOpenModal = () => setIsOpen(true)\n  const handleCloseModal = () => setIsOpen(false)\n\n  return (\n    <>\n      <Button variant=\"primary\" onClick={handleOpenModal}>\n        Sign in\n      </Button>\n\n      <Dialog open={isOpen} onClose={handleCloseModal} className={s.dialog}>\n        <DialogHeader />\n\n        <DialogContent className={s.content}>\n          <Typography variant=\"h2\">\n            Millions of Songs. <br /> Free on Musicfun.\n          </Typography>\n\n          <div className={s.icon}>😊</div>\n\n          <Button className={clsx(s.button, s.secondary)} fullWidth onClick={handleCloseModal}>\n            Continue without Sign in\n          </Button>\n          <Button\n            as=\"a\"\n            href=\"https://apihub.it-incubator.io/en/login\"\n            target=\"_blank\"\n            className={s.button}\n            variant=\"primary\"\n            fullWidth\n            onClick={handleCloseModal}>\n            Sign in with APIHub\n          </Button>\n        </DialogContent>\n      </Dialog>\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/ui/LoginButtonAndModal/index.ts",
    "content": "export { LoginButtonAndModal } from './LoginButtonAndModal'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.module.css",
    "content": ".trigger {\n  cursor: pointer;\n  display: flex;\n  gap: 8px;\n  align-items: center;\n}\n\n.avatar {\n  overflow: hidden;\n  width: 34px;\n  height: 34px;\n  border-radius: 50%;\n}\n\n.name {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ProfileDropdownMenu } from './ProfileDropdownMenu'\n\nconst meta: Meta<typeof ProfileDropdownMenu> = {\n  title: 'entities/ProfileDropdownMenu',\n  component: ProfileDropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof ProfileDropdownMenu>\n\nexport const Default: Story = {\n  args: {\n    avatar: 'https://unsplash.it/182/182',\n    name: 'Kanye West',\n    id: '1',\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/ui/ProfileDropdownMenu/ProfileDropdownMenu.tsx",
    "content": "import { Link } from 'react-router'\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Typography,\n} from '@/shared/components'\nimport { LogoutIcon, ProfileIcon } from '@/shared/icons'\n\nimport s from './ProfileDropdownMenu.module.css'\n\nexport const ProfileDropdownMenu = ({\n  avatar,\n  name,\n  id,\n}: {\n  avatar: string\n  name: string\n  id: string\n}) => {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild className={s.trigger}>\n        <div className={s.avatar}>\n          <img src={avatar} alt={''} />\n        </div>\n\n        <Typography className={s.name} variant=\"body2\">\n          {name}\n        </Typography>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem as={Link} to={`/user/${id}`}>\n          <ProfileIcon />\n          <span>My Profile</span>\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => {}}>\n          <LogoutIcon />\n          <span>Logout</span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/ui/ProfileDropdownMenu/index.ts",
    "content": "export * from './ProfileDropdownMenu'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/auth/ui/index.ts",
    "content": "export * from './LoginButtonAndModal'\nexport * from './ProfileDropdownMenu'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/api/index.ts",
    "content": "export * from './playlistsApi'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/api/playlistsApi.ts",
    "content": "export const playlistsApi = {}\n\nexport enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = 2,\n}\n\nexport const MOCK_PLAYLISTS = [\n  {\n    data: {\n      id: '1',\n      type: 'playlists',\n      attributes: {\n        title: 'Chill Vibes',\n        description: {\n          text: 'Relax and unwind with these chill tracks 🌊',\n        },\n        addedAt: '2025-06-01T12:00:00Z',\n        updatedAt: '2025-06-10T15:30:00Z',\n        order: 1,\n        user: {\n          id: 'user-101',\n          name: 'Alice',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 640,\n              height: 640,\n              fileSize: 204800,\n              url: 'https://unsplash.it/183/183',\n            },\n          ],\n        },\n        tags: ['chill', 'lofi', 'relax'],\n        currentUserReaction: CurrentUserReaction.Like,\n        likesCount: 542,\n      },\n    },\n  },\n  {\n    data: {\n      id: '2',\n      type: 'playlists',\n      attributes: {\n        title: 'Workout Pump',\n        description: {\n          text: 'High energy tracks to keep you moving 💪',\n        },\n        addedAt: '2025-05-20T08:00:00Z',\n        updatedAt: '2025-06-05T18:00:00Z',\n        order: 2,\n        user: {\n          id: 'user-202',\n          name: 'Bob',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 800,\n              height: 800,\n              fileSize: 307200,\n              url: 'https://unsplash.it/184/184',\n            },\n          ],\n        },\n        tags: ['fitness', 'pump', 'motivation'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 123,\n      },\n    },\n  },\n  {\n    data: {\n      id: '3',\n      type: 'playlists',\n      attributes: {\n        title: 'Fantasy Soundtrack',\n        description: {\n          text: 'Epic and magical music for your quests 🏹',\n        },\n        addedAt: '2025-04-15T14:30:00Z',\n        updatedAt: '2025-05-01T10:10:00Z',\n        order: 3,\n        user: {\n          id: 'user-303',\n          name: 'Elrond',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 1024,\n              height: 768,\n              fileSize: 512000,\n              url: 'https://unsplash.it/185/185',\n            },\n          ],\n        },\n        tags: ['fantasy', 'soundtrack', 'epic'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 54,\n      },\n    },\n  },\n  {\n    data: {\n      id: '4',\n      type: 'playlists',\n      attributes: {\n        title: 'Suffer possible assume',\n        description: {\n          text: 'Recently religious responsibility whether only.',\n        },\n        addedAt: '2025-04-29T10:39:13',\n        updatedAt: '2025-06-14T21:01:35',\n        order: 4,\n        user: {\n          id: 'user-4',\n          name: 'Katie',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 936,\n              height: 306,\n              fileSize: 243840,\n              url: 'https://unsplash.it/192/192',\n            },\n          ],\n        },\n        tags: ['any', 'shake', 'white'],\n        currentUserReaction: CurrentUserReaction.Like,\n        likesCount: 3,\n      },\n    },\n  },\n  {\n    data: {\n      id: '5',\n      type: 'playlists',\n      attributes: {\n        title: 'Risk still',\n        description: {\n          text: 'Skin pay sure yeah couple live heart.',\n        },\n        addedAt: '2025-01-26T00:52:16',\n        updatedAt: '2025-06-14T21:00:56',\n        order: 5,\n        user: {\n          id: 'user-5',\n          name: 'Robert',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 525,\n              height: 500,\n              fileSize: 185000,\n              url: 'https://unsplash.it/191/191',\n            },\n          ],\n        },\n        tags: ['term', 'item'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 12,\n      },\n    },\n  },\n  {\n    data: {\n      id: '6',\n      type: 'playlists',\n      attributes: {\n        title: 'Attack through go',\n        description: {\n          text: 'Plan deep sport growth tonight.',\n        },\n        addedAt: '2025-04-07T10:16:19',\n        updatedAt: '2025-06-14T21:02:28',\n        order: 6,\n        user: {\n          id: 'user-6',\n          name: 'Shelly',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 985,\n              height: 44,\n              fileSize: 105000,\n              url: 'https://unsplash.it/190/190',\n            },\n          ],\n        },\n        tags: ['feeling', 'size'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 0,\n      },\n    },\n  },\n  {\n    data: {\n      id: '7',\n      type: 'playlists',\n      attributes: {\n        title: 'Yet woman outside',\n        description: {\n          text: 'Attorney especially child music capital well.',\n        },\n        addedAt: '2025-01-02T16:37:47',\n        updatedAt: '2025-06-14T21:03:26',\n        order: 7,\n        user: {\n          id: 'user-7',\n          name: 'Kristopher',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 541,\n              height: 589,\n              fileSize: 312000,\n              url: 'https://unsplash.it/189/189',\n            },\n          ],\n        },\n        tags: ['week'],\n        currentUserReaction: CurrentUserReaction.Like,\n        likesCount: 12,\n      },\n    },\n  },\n  {\n    data: {\n      id: '8',\n      type: 'playlists',\n      attributes: {\n        title: 'Community',\n        description: {\n          text: 'Visit about occur it fast industry process.',\n        },\n        addedAt: '2025-06-03T22:12:23',\n        updatedAt: '2025-06-14T21:00:31',\n        order: 8,\n        user: {\n          id: 'user-8',\n          name: 'Kimberly',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 376,\n              height: 803,\n              fileSize: 460000,\n              url: 'https://unsplash.it/188/188',\n            },\n          ],\n        },\n        tags: ['serve', 'although', 'item'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 12,\n      },\n    },\n  },\n  {\n    data: {\n      id: '9',\n      type: 'playlists',\n      attributes: {\n        title: 'Dance Lights Forever',\n        description: {\n          text: 'Feel the beat drop and the lights flash 🎉',\n        },\n        addedAt: '2024-12-14T15:20:12',\n        updatedAt: '2025-06-13T17:15:00',\n        order: 9,\n        user: {\n          id: 'user-9',\n          name: 'Jasmine',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 800,\n              height: 800,\n              fileSize: 310000,\n              url: 'https://unsplash.it/187/187',\n            },\n          ],\n        },\n        tags: ['dance', 'party', 'electro'],\n        currentUserReaction: CurrentUserReaction.None,\n        likesCount: 2,\n      },\n    },\n  },\n  {\n    data: {\n      id: '10',\n      type: 'playlists',\n      attributes: {\n        title: 'Calm Forest Ambience',\n        description: {\n          text: 'Let nature help you concentrate 🌲',\n        },\n        addedAt: '2025-03-01T09:45:00',\n        updatedAt: '2025-06-10T13:20:00',\n        order: 10,\n        user: {\n          id: 'user-10',\n          name: 'Leo',\n        },\n        images: {\n          main: [\n            {\n              type: 'original',\n              width: 1024,\n              height: 576,\n              fileSize: 280000,\n              url: 'https://unsplash.it/186/186',\n            },\n          ],\n        },\n        tags: ['nature', 'focus', 'relax'],\n        currentUserReaction: CurrentUserReaction.Dislike,\n        likesCount: 84,\n      },\n    },\n  },\n]\n\nexport const MOCK_PLAYLIST = {\n  data: {\n    id: '10',\n    type: 'playlists',\n    attributes: {\n      title: 'Calm Forest Ambience',\n      description: {\n        text: 'Let nature help you concentrate 🌲',\n      },\n      addedAt: '2025-03-01T09:45:00',\n      updatedAt: '2025-06-10T13:20:00',\n      order: 10,\n      user: {\n        id: 'user-10',\n        name: 'Leo',\n      },\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 1024,\n            height: 576,\n            fileSize: 280000,\n            url: 'https://unsplash.it/300/300',\n          },\n        ],\n      },\n      tags: ['nature', 'focus', 'relax'],\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 12,\n    },\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/CreatePlaylistModal/CreatePlaylistModal.module.css",
    "content": ".dialog {\n  width: 100%;\n  max-width: 745px;\n}\n\n.form {\n  overflow-y: auto;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n  margin-bottom: 16px;\n}\n\n.imageUploader {\n  width: 280px;\n  margin: 0 auto;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/CreatePlaylistModal/CreatePlaylistModal.tsx",
    "content": "import { useState } from 'react'\n\nimport {\n  Button,\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  ImageUploader,\n  TagEditor,\n  Textarea,\n  TextField,\n  Typography,\n} from '@/shared/components'\n\nimport s from './CreatePlaylistModal.module.css'\n\nexport const CreatePlaylistModal = ({ onClose }: { onClose: () => void }) => {\n  const [tags, setTags] = useState<string[]>([])\n  const handleTagsChange = (tags: string[]) => {\n    setTags(tags)\n  }\n\n  return (\n    <Dialog open onClose={onClose} className={s.dialog}>\n      <DialogHeader>\n        <Typography variant=\"h2\">Create Playlist</Typography>\n      </DialogHeader>\n\n      <form className={s.form}>\n        <DialogContent className={s.content}>\n          <ImageUploader className={s.imageUploader} onImageSelect={() => {}} />\n          <TextField label=\"Title\" placeholder=\"Enter playlist title\" />\n          <Textarea rows={3} label=\"Description\" placeholder=\"Enter playlist description\" />\n          <TagEditor label=\"Hashtags\" value={tags} onTagsChange={handleTagsChange} maxTags={5} />\n        </DialogContent>\n\n        <DialogFooter>\n          <Button variant=\"secondary\" onClick={onClose} type=\"button\">\n            Cancel\n          </Button>\n          <Button variant=\"primary\" type=\"submit\">\n            Create\n          </Button>\n        </DialogFooter>\n      </form>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/CreatePlaylistModal/index.ts",
    "content": "export * from './CreatePlaylistModal'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/PlaylistCard/PlaylistCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n  width: 264px;\n}\n\n.image {\n  overflow: hidden;\n  height: 153px;\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.description {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/PlaylistCard/PlaylistCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { CurrentUserReaction } from '../../api'\nimport { PlaylistCard } from './PlaylistCard'\n\nconst meta: Meta<typeof PlaylistCard> = {\n  title: 'entities/PlaylistCard',\n  component: PlaylistCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistCard>\n\nexport const Default: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    image: 'https://unsplash.it/182/182',\n    description: 'A playlist for relaxing and unwinding.',\n  },\n}\n\nexport const WithReactions: Story = {\n  args: {\n    id: '1',\n    title: 'Lofi for Vibe Coding',\n    image: 'https://unsplash.it/182/182',\n    description: 'A playlist for relaxing and unwinding.',\n    isShowReactionButtons: true,\n    reaction: CurrentUserReaction.Like,\n    likesCount: 10,\n    onLike: () => {},\n    onDislike: () => {},\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    id: '1',\n    title: 'The Best Hits of Elton John',\n    image: 'https://unsplash.it/183/183',\n    description:\n      'A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding. A playlist for relaxing and unwinding.',\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/PlaylistCard/PlaylistCard.tsx",
    "content": "import { Link } from 'react-router'\n\nimport { Card, ReactionButtons, type ReactionButtonsProps, Typography } from '@/shared/components'\n\nimport s from './PlaylistCard.module.css'\n\ntype PlaylistCardPropsBase = {\n  id: string\n  title: string\n  image: string\n  description: string\n}\n\ntype PlaylistCardPropsWithReactions = PlaylistCardPropsBase & {\n  isShowReactionButtons: true\n} & Omit<ReactionButtonsProps, 'className'>\n\ntype PlaylistCardPropsWithoutReactions = PlaylistCardPropsBase & {\n  isShowReactionButtons?: false\n}\n\ntype PlaylistCardProps = PlaylistCardPropsWithReactions | PlaylistCardPropsWithoutReactions\n\nexport const PlaylistCard = ({\n  title,\n  image,\n  description,\n  id,\n  isShowReactionButtons,\n  ...props\n}: PlaylistCardProps) => {\n  return (\n    <Card as={Link} to={`/playlists/${id}`} className={s.card}>\n      <div className={s.image}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n      <Typography variant=\"h3\" className={s.title}>\n        {title}\n      </Typography>\n      <Typography variant=\"body3\" className={s.description}>\n        {description}\n      </Typography>\n      {/*  'reaction' in props — Type guard for correct type checking */}\n      {isShowReactionButtons && 'reaction' in props && (\n        <ReactionButtons\n          reaction={props.reaction}\n          onLike={props.onLike}\n          onDislike={props.onDislike}\n          likesCount={props.likesCount}\n        />\n      )}\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/PlaylistCard/index.ts",
    "content": "export * from './PlaylistCard'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.module.css",
    "content": ".container {\n  display: flex;\n  gap: 24px;\n  background: transparent;\n}\n\n.imageContainer {\n  flex-shrink: 0;\n  width: 297px;\n  height: 297px;\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  min-width: 0;\n}\n\n.title {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  margin-bottom: 8px;\n\n  font-size: clamp(var(--font-size-xxl), 8vw, var(--font-size-xxxl));\n  font-weight: 900;\n  line-height: 1;\n  white-space: pre-wrap;\n}\n\n.description {\n  opacity: 0.7;\n}\n\n.info {\n  margin-top: auto;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_5_HASHTAGS } from '@/features/tags'\n\nimport { PlaylistOverview } from '../PlaylistOverview'\n\nconst meta: Meta<typeof PlaylistOverview> = {\n  title: 'entities/PlaylistOverview',\n  component: PlaylistOverview,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof PlaylistOverview>\n\nexport const Default: Story = {\n  args: {\n    title: 'Chill Mix',\n    image: 'https://unsplash.it/297/297',\n    description: 'Julia Wolf, ayokay, Khalid and more',\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n\nexport const LongTitle: Story = {\n  args: {\n    title: 'This is a Very Long Playlist Title That Should Scale Responsively',\n    image: 'https://unsplash.it/299/299',\n    description: 'A collection of amazing tracks from various artists around the world',\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/PlaylistOverview/PlaylistOverview.tsx",
    "content": "import clsx from 'clsx'\nimport { type ComponentProps } from 'react'\n\nimport { TagsList } from '@/features/tags'\nimport { Typography } from '@/shared/components'\n\nimport s from './PlaylistOverview.module.css'\n\ntype PlaylistOverviewProps = {\n  title: string\n  image: string\n  description: string\n  tags: string[]\n} & ComponentProps<'div'>\n\nexport const PlaylistOverview = ({\n  title,\n  image,\n  description,\n  tags,\n  className,\n  ...props\n}: PlaylistOverviewProps) => {\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <div className={s.imageContainer}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <div className={s.content}>\n        <TagsList tags={tags} entity=\"playlists\" />\n\n        <Typography variant=\"h1\" as=\"h1\" className={s.title}>\n          {title}\n        </Typography>\n\n        <div className={s.info}>\n          <Typography variant=\"body1\" className={s.description}>\n            {description}\n          </Typography>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/PlaylistOverview/index.ts",
    "content": "export * from './PlaylistOverview'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/playlists/ui/index.ts",
    "content": "export * from './CreatePlaylistModal'\nexport * from './PlaylistCard'\nexport * from './PlaylistOverview'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tags/api/index.ts",
    "content": "export * from './tags-api'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tags/api/tags-api.ts",
    "content": "export const MOCK_HASHTAGS = [\n  'Rock',\n  'Jazz',\n  'Blues',\n  'Metal',\n  'Folk',\n  'Coding',\n  'Dark Ambient',\n  'Chill',\n  'Lo-fi',\n]\n\nexport const MOCK_5_HASHTAGS = MOCK_HASHTAGS.slice(0, 5)\n\nexport type TagDto = {\n  id: string\n  name: string\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tags/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tags/ui/TagsList/TagsList.module.css",
    "content": ".list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tags/ui/TagsList/TagsList.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_HASHTAGS } from '../../api/tags-api'\nimport { TagsList } from './TagsList'\n\nconst meta: Meta<typeof TagsList> = {\n  title: 'entities/TagsList',\n  component: TagsList,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TagsList>\n\nexport const Default: Story = {\n  args: {\n    tags: MOCK_HASHTAGS,\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tags/ui/TagsList/TagsList.tsx",
    "content": "import { Link } from 'react-router'\n\nimport { Tag } from '@/shared/components'\n\nimport s from './TagsList.module.css'\n\nexport const TagsList = ({\n  tags,\n  entity = 'tracks',\n}: {\n  tags: string[]\n  entity?: 'tracks' | 'playlists'\n}) => {\n  return (\n    <ul className={s.list}>\n      {tags.map((tag) => (\n        <li key={tag}>\n          <Tag as={Link} to={`/${entity}?tag=${tag}`} tag={tag} />\n        </li>\n      ))}\n    </ul>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tags/ui/TagsList/index.ts",
    "content": "export { TagsList } from './TagsList'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tags/ui/index.ts",
    "content": "export * from './'\nexport * from './TagsList'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/api/index.ts",
    "content": "export * from './tracksApi'\nexport * from './types'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/api/tracksApi.ts",
    "content": "enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = 2,\n}\n\nexport const MOCK_TRACKS = [\n  {\n    id: '1',\n    type: 'tracks',\n    attributes: {\n      artist: 'Headlund',\n      id: '1',\n      title: 'Days That Matter',\n      addedAt: '2025-06-01T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/110/110',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 104,\n      dislikesCount: 2,\n      artists: [{ id: '1', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '2',\n    type: 'tracks',\n    attributes: {\n      artist: 'Stellar Wave',\n      id: '2',\n      title: 'Cosmic Dust',\n      addedAt: '2025-06-02T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/111/111',\n          },\n        ],\n      },\n      user: {\n        id: '2',\n        name: 'Jane Smith',\n      },\n      currentUserReaction: CurrentUserReaction.Like,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '2', name: 'Jane Smith' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '3',\n    type: 'tracks',\n    attributes: {\n      artist: 'Aqua Marine',\n      id: '3',\n      title: 'Ocean Breath Is The Best Track Ever',\n      addedAt: '2025-06-03T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/112/112',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 1,\n      dislikesCount: 2,\n      artists: [\n        { id: '3', name: 'Peter Jones' },\n        { id: '4', name: 'Chris Green' },\n        { id: '5', name: 'John Doe' },\n      ],\n      duration: 100,\n    },\n  },\n  {\n    id: '4',\n    type: 'tracks',\n    attributes: {\n      artist: 'Night Rider',\n      id: '4',\n      title: 'Midnight Drive',\n      addedAt: '2025-06-04T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/113/113',\n          },\n        ],\n      },\n      user: {\n        id: '3',\n        name: 'Peter Jones',\n      },\n      currentUserReaction: CurrentUserReaction.Dislike,\n      likesCount: 666,\n      dislikesCount: 2,\n      artists: [{ id: '4', name: 'Chris Green' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '5',\n    type: 'tracks',\n    attributes: {\n      artist: 'Urban Glow',\n      id: '5',\n      title: 'City Lights',\n      addedAt: '2025-06-05T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/114/114',\n          },\n        ],\n      },\n      user: {\n        id: '2',\n        name: 'Jane Smith',\n      },\n      currentUserReaction: CurrentUserReaction.Like,\n      likesCount: 8,\n      dislikesCount: 2,\n      artists: [{ id: '5', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '6',\n    type: 'tracks',\n    attributes: {\n      artist: 'Whispering Pines',\n      id: '6',\n      title: 'Forest Lullaby',\n      addedAt: '2025-06-06T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/115/115',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 1,\n      dislikesCount: 2,\n      duration: 100,\n    },\n  },\n  {\n    id: '7',\n    type: 'tracks',\n    attributes: {\n      artist: 'Sandstorm',\n      id: '7',\n      title: 'Desert Mirage',\n      addedAt: '2025-06-07T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/116/116',\n          },\n        ],\n      },\n      user: {\n        id: '4',\n        name: 'Susan Lee',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '7', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '8',\n    type: 'tracks',\n    attributes: {\n      artist: 'Altitude',\n      id: '8',\n      title: 'Mountain Peak',\n      addedAt: '2025-06-08T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/117/117',\n          },\n        ],\n      },\n      user: {\n        id: '3',\n        name: 'Peter Jones',\n      },\n      currentUserReaction: CurrentUserReaction.Like,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '8', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '9',\n    type: 'tracks',\n    attributes: {\n      artist: 'Water Lily',\n      id: '9',\n      title: 'River Flow',\n      addedAt: '2025-06-09T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/118/118',\n          },\n        ],\n      },\n      user: {\n        id: '1',\n        name: 'John Doe',\n      },\n      currentUserReaction: CurrentUserReaction.Dislike,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '10', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n  {\n    id: '10',\n    type: 'tracks',\n    attributes: {\n      artist: 'Galaxy Explorer',\n      id: '10',\n      title: 'Final Frontier',\n      addedAt: '2025-06-10T12:00:00Z',\n      attachments: [],\n      images: {\n        main: [\n          {\n            type: 'original',\n            width: 100,\n            height: 100,\n            fileSize: 0,\n            url: 'https://unsplash.it/119/119',\n          },\n        ],\n      },\n      user: {\n        id: '5',\n        name: 'Chris Green',\n      },\n      currentUserReaction: CurrentUserReaction.None,\n      likesCount: 10,\n      dislikesCount: 2,\n      artists: [{ id: '10', name: 'John Doe' }],\n      duration: 100,\n    },\n  },\n]\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/api/types.ts",
    "content": "export enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = 2,\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/index.ts",
    "content": "export * from './api'\nexport * from './ui'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackCard/TrackCard.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n  width: 128px;\n}\n\n.image {\n  overflow: hidden;\n  height: 103px;\n  transition:\n    opacity 0.2s,\n    transform 0.4s;\n}\n\n.card:hover .image {\n  transform: scale(1.02);\n  opacity: 0.92;\n}\n\n.image img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.artists {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  text-overflow: ellipsis;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackCard/TrackCard.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { TrackCard } from './TrackCard'\n\nconst meta: Meta<typeof TrackCard> = {\n  title: 'entities/TrackCard',\n  component: TrackCard,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TrackCard>\n\nexport const Default: Story = {\n  args: {\n    id: '1',\n    title: 'Name Song',\n    image: 'https://unsplash.it/182/182',\n    artists: 'Ed Sheeran, Big Sean, Juice W...',\n  },\n}\n\nexport const WithLongTextContent: Story = {\n  args: {\n    id: '1',\n    title: 'A very long track title that should be truncated',\n    image: 'https://unsplash.it/183/183',\n    artists:\n      'A lot of artists on this track, so many that the text should overflow and be truncated by ellipsis',\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackCard/TrackCard.tsx",
    "content": "import { Link } from 'react-router'\n\nimport { Card, ReactionButtons, type ReactionButtonsProps, Typography } from '@/shared/components'\n\nimport s from './TrackCard.module.css'\n\ntype Props = {\n  id: string\n  image: string\n  title: string\n  artists: string\n} & Omit<ReactionButtonsProps, 'className'>\n\nexport const TrackCard = ({\n  id,\n  image,\n  title,\n  artists,\n  reaction,\n  onLike,\n  onDislike,\n  likesCount,\n}: Props) => {\n  return (\n    <Card as={Link} to={`/tracks/${id}`} className={s.card}>\n      <div className={s.image}>\n        <img src={image} alt={title} />\n      </div>\n\n      <Typography variant=\"h3\" className={s.title}>\n        {title}\n      </Typography>\n\n      <Typography variant=\"body3\" className={s.artists}>\n        {artists}\n      </Typography>\n      <ReactionButtons\n        reaction={reaction}\n        onLike={onLike}\n        onDislike={onDislike}\n        likesCount={likesCount}\n      />\n    </Card>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackCard/index.ts",
    "content": "export * from './TrackCard'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackInfoCell/TrackInfoCell.module.css",
    "content": ".box {\n  display: flex;\n  gap: 21px;\n}\n\n.image {\n  flex-shrink: 0;\n  width: 52px;\n  height: 52px;\n}\n\n.image img {\n  object-fit: cover;\n}\n\n.info {\n  width: 228px;\n}\n\n.title {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.title.playing {\n  color: var(--color-accent);\n}\n\n.artists {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackInfoCell/TrackInfoCell.tsx",
    "content": "import clsx from 'clsx'\nimport { Link } from 'react-router'\n\nimport { TableCell, Typography } from '@/shared/components'\n\nimport s from './TrackInfoCell.module.css'\n\nexport const TrackInfoCell = ({\n  image,\n  title,\n  artists,\n  isPlaying,\n  id,\n}: {\n  image: string\n  title: string\n  artists: string[]\n  isPlaying: boolean\n  id: string\n}) => {\n  return (\n    <TableCell>\n      <div className={s.box}>\n        <div className={s.image}>\n          <img src={image} alt={title} />\n        </div>\n        <div className={s.info}>\n          <Typography\n            variant=\"body1\"\n            as={Link}\n            className={clsx(s.title, isPlaying && s.playing)}\n            to={`/tracks/${id}`}>\n            {title}\n          </Typography>\n          <Typography className={s.artists} variant=\"body2\">\n            {artists.join(', ')}\n          </Typography>\n        </div>\n      </div>\n    </TableCell>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackInfoCell/index.ts",
    "content": "export * from './TrackInfoCell.tsx'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackOverview/TrackOverview.module.css",
    "content": ".container {\n  display: flex;\n  gap: 24px;\n  background: transparent;\n}\n\n.imageContainer {\n  flex-shrink: 0;\n  width: 297px;\n  height: 297px;\n}\n\n.imageContainer img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.content {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  min-width: 0;\n}\n\n.title {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n\n  margin-bottom: 8px;\n\n  font-size: clamp(var(--font-size-xxl), 8vw, var(--font-size-xxxl));\n  font-weight: 900;\n  line-height: 1;\n  white-space: pre-wrap;\n}\n\n.description {\n  opacity: 0.7;\n}\n\n.info {\n  margin-top: auto;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackOverview/TrackOverview.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { MOCK_5_HASHTAGS } from '@/features/tags'\n\nimport { TrackOverview } from './TrackOverview'\n\nconst meta: Meta<typeof TrackOverview> = {\n  title: 'entities/TrackOverview',\n  component: TrackOverview,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TrackOverview>\n\nexport const Default: Story = {\n  args: {\n    title: 'Chill Mix',\n    image: 'https://unsplash.it/297/297',\n    releaseDate: '2025-01-01',\n    artists: ['Julia Wolf', 'ayokay', 'Khalid'],\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n\nexport const LongTitle: Story = {\n  args: {\n    title: 'This is a Very Long Track Title That Should Scale Responsively',\n    image: 'https://unsplash.it/299/299',\n    releaseDate: '2025-01-01',\n    artists: ['Julia Wolf', 'ayokay', 'Khalid'],\n    tags: MOCK_5_HASHTAGS,\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackOverview/TrackOverview.tsx",
    "content": "import clsx from 'clsx'\nimport { type ComponentProps } from 'react'\n\nimport { TagsList } from '@/features/tags'\nimport { Typography } from '@/shared/components'\n\nimport s from './TrackOverview.module.css'\n\ntype TrackOverviewProps = {\n  title: string\n  image: string\n  releaseDate: string\n  artists: string[]\n  tags: string[]\n} & ComponentProps<'div'>\n\nexport const TrackOverview = ({\n  title,\n  image,\n  releaseDate,\n  tags,\n  className,\n  artists,\n  ...props\n}: TrackOverviewProps) => {\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <div className={s.imageContainer}>\n        <img src={image} alt=\"\" aria-hidden />\n      </div>\n\n      <div className={s.content}>\n        <TagsList tags={tags} entity=\"tracks\" />\n\n        <Typography variant=\"h1\" as=\"h1\" className={s.title}>\n          {title}\n        </Typography>\n\n        <div className={s.info}>\n          <Typography variant=\"body1\">{artists.join(', ')}</Typography>\n          <Typography variant=\"body2\">{releaseDate}</Typography>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackOverview/index.ts",
    "content": "export * from './TrackOverview'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackRow/TrackRow.module.css",
    "content": ".playing {\n  color: var(--color-accent);\n}\n\n.progress {\n  width: 183px;\n}\n\n.actions {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TrackRow/TrackRow.tsx",
    "content": "import clsx from 'clsx'\nimport type { ReactNode } from 'react'\n\nimport { Progress, TableCell, TableRow, Typography } from '@/shared/components'\nimport { LiveWaveIcon } from '@/shared/icons'\n\nimport { TrackInfoCell } from '../TrackInfoCell'\nimport type { TrackRowData } from '../TracksTable/TracksTable.tsx'\nimport s from './TrackRow.module.css'\n\nexport const TrackRow = <T extends TrackRowData>({\n  trackRow,\n  playingTrackId,\n  playingTrackProgress,\n  renderActionsCell,\n}: {\n  renderActionsCell: (trackRow: T) => ReactNode\n  trackRow: T\n  playingTrackId?: string\n  playingTrackProgress?: number\n}) => {\n  const isPlaying = playingTrackId === trackRow.id\n\n  return (\n    <TableRow>\n      <TableCell className={clsx(isPlaying && s.playing)}>\n        {isPlaying ? <LiveWaveIcon /> : trackRow.index + 1}\n      </TableCell>\n      <TrackInfoCell\n        id={trackRow.id}\n        image={trackRow.image}\n        title={trackRow.title}\n        artists={trackRow.artists}\n        isPlaying={isPlaying}\n      />\n      <TableCell>\n        {isPlaying && (\n          <Progress\n            className={s.progress}\n            value={playingTrackProgress ?? 0}\n            max={trackRow.duration}\n          />\n        )}\n      </TableCell>\n      <TableCell>\n        <Typography variant=\"body2\" as=\"time\" dateTime={trackRow.addedAt}>\n          {new Date(trackRow.addedAt).toLocaleDateString()}\n        </Typography>\n      </TableCell>\n      <TableCell>\n        <div className={s.actions}>{renderActionsCell(trackRow)}</div>\n      </TableCell>\n      <TableCell>\n        <Typography variant=\"body2\">{trackRow.duration}</Typography>\n      </TableCell>\n    </TableRow>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TracksTable/TrackTable.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport {\n  CurrentUserReaction,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  ReactionButtons,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport { MOCK_TRACKS } from '../../api'\nimport { TracksTable } from './TracksTable'\n\nconst meta: Meta<typeof TracksTable> = {\n  title: 'entities/TracksTable',\n  component: TracksTable,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof TracksTable>\n\ntype ReactionsProps =\n  | {\n      likesCount: number\n      dislikesCount: number\n      currentUserReaction: CurrentUserReaction\n    }\n  | {\n      likesCount?: undefined\n      dislikesCount?: undefined\n      currentUserReaction?: undefined\n    }\n\nexport type TrackRowData = {\n  index: number\n  image: string\n  id: string\n  title: string\n  addedAt: string\n  artists: string[]\n  duration: number\n} & ReactionsProps\n\nexport const Default: Story = {\n  args: {\n    trackRows: MOCK_TRACKS.map((track, index) => ({\n      index: index,\n      id: track.id,\n      title: track.attributes.title,\n      image: track.attributes.images.main[0].url,\n      addedAt: track.attributes.addedAt,\n      artists: track.attributes.artists?.map((artist) => artist.name) || [],\n      isPlaying: false,\n      likesCount: track.attributes.likesCount,\n      dislikesCount: track.attributes.dislikesCount,\n      currentUserReaction: track.attributes.currentUserReaction,\n      duration: track.attributes.duration,\n    })),\n    renderTrackRow: (trackRow) => (\n      <TrackRow\n        trackRow={trackRow}\n        playingTrackId={MOCK_TRACKS[0].id}\n        playingTrackProgress={20}\n        renderActionsCell={() => (\n          <>\n            <ReactionButtons\n              reaction={trackRow.currentUserReaction}\n              onLike={() => {}}\n              onDislike={() => {}}\n              likesCount={trackRow.likesCount}\n            />\n\n            <DropdownMenu>\n              <DropdownMenuTrigger>\n                <MoreIcon />\n              </DropdownMenuTrigger>\n\n              <DropdownMenuContent>\n                <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                  Add to playlist\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                  Show text song\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </>\n        )}\n      />\n    ),\n  },\n}\n\nexport const WithoutReactions: Story = {\n  args: {\n    trackRows: MOCK_TRACKS.map((track, index) => ({\n      index: index,\n      id: track.id,\n      title: track.attributes.title,\n      image: track.attributes.images.main[0].url,\n      addedAt: track.attributes.addedAt,\n      artists: track.attributes.artists?.map((artist) => artist.name) || [],\n      duration: track.attributes.duration,\n    })),\n    renderTrackRow: (trackRow) => (\n      <TrackRow\n        trackRow={trackRow}\n        playingTrackId={MOCK_TRACKS[0].id}\n        playingTrackProgress={20}\n        renderActionsCell={() => (\n          <div>\n            <DropdownMenu>\n              <DropdownMenuTrigger>\n                <MoreIcon />\n              </DropdownMenuTrigger>\n\n              <DropdownMenuContent>\n                <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                  Add to playlist\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                  Show text song\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        )}\n      />\n    ),\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TracksTable/TracksTable.tsx",
    "content": "import type { ReactNode } from 'react'\n\nimport {\n  CurrentUserReaction,\n  Table,\n  TableBody,\n  TableHead,\n  TableHeaderCell,\n  TableRow,\n} from '@/shared/components'\nimport { ClockIcon } from '@/shared/icons'\n\ntype TableColumn = {\n  title: ReactNode\n  width?: string\n}\n\nconst TABLE_COLUMNS: TableColumn[] = [\n  {\n    title: '#',\n    width: '40px',\n  },\n  {\n    title: 'Track',\n  },\n  {\n    title: '',\n  },\n  {\n    title: 'Date added',\n    width: '120px',\n  },\n  {\n    title: 'Actions',\n    width: '150px',\n  },\n  {\n    title: <ClockIcon />,\n    width: '60px',\n  },\n]\n\nexport type TracksTableProps<T extends TrackRowData> = {\n  trackRows: T[]\n  renderTrackRow: (trackRow: T) => ReactNode\n}\n\ntype ReactionsProps =\n  | {\n      likesCount: number\n      dislikesCount: number\n      currentUserReaction: CurrentUserReaction\n    }\n  | {\n      likesCount?: undefined\n      dislikesCount?: undefined\n      currentUserReaction?: undefined\n    }\n\nexport type TrackRowData = {\n  index: number\n  image: string\n  id: string\n  title: string\n  addedAt: string\n  artists: string[]\n  duration: number\n} & ReactionsProps\n\nexport const TracksTable = <T extends TrackRowData>({\n  trackRows,\n  renderTrackRow,\n}: TracksTableProps<T>) => {\n  return (\n    <Table>\n      <TableHead>\n        <TableRow>\n          {TABLE_COLUMNS.map((column, index) => (\n            <TableHeaderCell key={index} style={{ width: column.width }}>\n              {column.title}\n            </TableHeaderCell>\n          ))}\n        </TableRow>\n      </TableHead>\n      <TableBody>{trackRows.map((trackRow) => renderTrackRow(trackRow))}</TableBody>\n    </Table>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/TracksTable/index.ts",
    "content": "export * from './TracksTable'\n"
  },
  {
    "path": "apps/ui-vanilla/src/features/tracks/ui/index.ts",
    "content": "export * from './TrackCard'\nexport * from './TrackOverview'\nexport * from './TracksTable'\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Header/Header.module.css",
    "content": ".header {\n  display: flex;\n  grid-area: header;\n  align-items: center;\n  justify-content: space-between;\n\n  height: var(--header-height);\n  padding: 0 32px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Header/Header.tsx",
    "content": "import { LoginButtonAndModal, ProfileDropdownMenu } from '@/features/auth'\n\nimport s from './Header.module.css'\n\nconst IS_AUTH = true // temporary data\n\nexport const Header = () => {\n  return (\n    <header className={s.header}>\n      <div className={s.logo}>Musicfun</div>\n\n      {IS_AUTH ? (\n        <ProfileDropdownMenu avatar={'//unsplash.it/100/100'} name={'Martin Fowler'} id={'1'} />\n      ) : (\n        <LoginButtonAndModal />\n      )}\n    </header>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Header/index.ts",
    "content": "export { Header } from './Header'\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Layout.module.css",
    "content": ".grid {\n  display: grid;\n  grid-template: 'header header' var(--header-height) 'sidebar main' 1fr / 310px 1fr;\n  height: 100vh;\n}\n\n.grid.playerOpen {\n  grid-template:\n    'header header' var(--header-height)\n    'sidebar main' 1fr 'player player' var(--player-height) / 310px 1fr;\n}\n\n.main {\n  overflow-y: auto;\n  grid-area: main;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Layout.tsx",
    "content": "import clsx from 'clsx'\nimport { Outlet } from 'react-router'\n\nimport { Player } from '@/widgets/Player'\n\nimport { Header } from './Header'\nimport s from './Layout.module.css'\nimport { Sidebar } from './Sidebar'\n\nexport const Layout = () => {\n  const IS_PLAYER_OPEN = true\n\n  return (\n    <div className={clsx(s.grid, IS_PLAYER_OPEN && s.playerOpen)}>\n      <Header />\n      <Sidebar />\n      <main className={s.main}>\n        <Outlet />\n      </main>\n      {IS_PLAYER_OPEN && <Player />}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Sidebar/MenuLinks/MenuLinks.module.css",
    "content": ".column {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.list + .list {\n  padding-top: 20px;\n  border-top: 1px solid var(--color-bg-secondary);\n}\n\n.link {\n  all: unset;\n\n  cursor: pointer;\n\n  display: flex;\n  gap: 16px;\n  align-items: center;\n\n  width: fit-content;\n\n  font-size: var(--font-size-m);\n  font-weight: 700;\n  color: var(--color-text-secondary);\n\n  transition: color 0.2s ease;\n}\n\n.link:hover {\n  color: var(--color-text-primary);\n}\n\n.active {\n  color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Sidebar/MenuLinks/MenuLinks.tsx",
    "content": "import clsx from 'clsx'\nimport { NavLink } from 'react-router'\n\nimport { HomeIcon, LibraryIcon, PlaylistIcon, TrackIcon, UploadIcon } from '@/shared/icons'\nimport { CreateIcon } from '@/shared/icons/CreateIcon'\n\nimport s from './MenuLinks.module.css'\n\ntype MenuLink = {\n  to: string\n  icon: React.ReactNode\n  label: string\n}\n\ntype MenuButton = {\n  onClick: () => void\n  icon: React.ReactNode\n  label: string\n}\n\nconst mainLinks: MenuLink[] = [\n  {\n    to: '/',\n    icon: <HomeIcon width={32} height={32} />,\n    label: 'Home',\n  },\n  {\n    to: '/user/1',\n    icon: <LibraryIcon />,\n    label: 'Your Library',\n  },\n]\n\nconst createLinks: MenuLink[] = [\n  {\n    to: '/tracks',\n    icon: <TrackIcon />,\n    label: 'All Tracks',\n  },\n  {\n    to: '/playlists',\n    icon: <PlaylistIcon />,\n    label: 'All Playlists',\n  },\n]\n\nexport const MenuLinks = () => {\n  const actionButtons: MenuButton[] = [\n    {\n      onClick: () => {},\n      icon: <UploadIcon />,\n      label: 'Upload Track',\n    },\n    {\n      onClick: () => {},\n      icon: <CreateIcon />,\n      label: 'Create Playlist',\n    },\n  ]\n\n  return (\n    <nav className={s.column} aria-label=\"Main navigation\">\n      <ul className={s.list}>\n        {mainLinks.map((props) => (\n          <li key={props.to}>\n            <SidebarLink {...props} />\n          </li>\n        ))}\n      </ul>\n      <ul className={s.list}>\n        {actionButtons.map((props) => (\n          <li key={props.label}>\n            <SidebarButton {...props} />\n          </li>\n        ))}\n      </ul>\n      <ul className={s.list}>\n        {createLinks.map((props) => (\n          <li key={props.to}>\n            <SidebarLink {...props} />\n          </li>\n        ))}\n      </ul>\n    </nav>\n  )\n}\n\nconst SidebarLink = ({ to, icon, label }: MenuLink) => (\n  <NavLink to={to} className={({ isActive }) => clsx(s.link, isActive && s.active)}>\n    {icon}\n    {label}\n  </NavLink>\n)\n\nconst SidebarButton = ({ onClick, icon, label }: MenuButton) => (\n  <button onClick={onClick} className={s.link} type=\"button\">\n    {icon}\n    {label}\n  </button>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Sidebar/MenuLinks/index.ts",
    "content": "export * from './MenuLinks'\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Sidebar/Sidebar.module.css",
    "content": ".sidebar {\n  overflow-y: auto;\n  display: flex;\n  grid-area: sidebar;\n  flex-direction: column;\n\n  height: calc(100vh - var(--header-height) - var(--player-height));\n  padding: 0 30px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Sidebar/Sidebar.tsx",
    "content": "import { MenuLinks } from './MenuLinks'\nimport s from './Sidebar.module.css'\n\nexport const Sidebar = () => {\n  return (\n    <div className={s.sidebar}>\n      <MenuLinks />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/Sidebar/index.ts",
    "content": "export { Sidebar } from './Sidebar'\n"
  },
  {
    "path": "apps/ui-vanilla/src/layout/index.ts",
    "content": "export { Layout } from './Layout'\n"
  },
  {
    "path": "apps/ui-vanilla/src/main.tsx",
    "content": "import './styles/fonts.css'\nimport './styles/variables.css'\nimport './styles/reset.css'\nimport './styles/global.css'\n\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { BrowserRouter } from 'react-router'\n\nimport { App } from './app/App.tsx'\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </StrictMode>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/MainPage/MainPage.module.css",
    "content": ".mainPage {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n}\n\n.artistsList {\n  --list-gap: 24px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/MainPage/MainPage.tsx",
    "content": "import { MOCK_PLAYLISTS, PlaylistCard } from '@/features/playlists'\nimport { MOCK_HASHTAGS, TagsList } from '@/features/tags'\nimport { MOCK_TRACKS, TrackCard } from '@/features/tracks'\n\nimport { ContentList, PageWrapper } from '../common'\nimport s from './MainPage.module.css'\n\nexport const MainPage = () => {\n  return (\n    <PageWrapper className={s.mainPage}>\n      <TagsList tags={MOCK_HASHTAGS} />\n      <ContentList\n        title=\"New playlists\"\n        data={MOCK_PLAYLISTS}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            image={playlist.data.attributes.images.main[0].url}\n            description={playlist.data.attributes.description.text}\n            isShowReactionButtons={true}\n            reaction={playlist.data.attributes.currentUserReaction}\n            onLike={() => {}}\n            onDislike={() => {}}\n            likesCount={playlist.data.attributes.likesCount}\n          />\n        )}\n      />\n      <ContentList\n        title=\"New tracks\"\n        data={MOCK_TRACKS}\n        renderItem={(track) => (\n          <TrackCard\n            artists={track.attributes.artist}\n            title={track.attributes.title}\n            id={track.id}\n            image={track.attributes.images.main[0].url}\n            reaction={track.attributes.currentUserReaction}\n            onDislike={() => {}}\n            onLike={() => {}}\n            likesCount={track.attributes.likesCount}\n          />\n        )}\n      />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/MainPage/index.ts",
    "content": "export * from './MainPage'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistPage/PlaylistPage.module.css",
    "content": ".playlistPage {\n  --page-gradient-color: #adbf22;\n}\n\n.playlistOverview {\n  margin-bottom: 46px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistPage/PlaylistPage.tsx",
    "content": "import { MOCK_PLAYLIST, PlaylistOverview } from '@/features/playlists'\nimport { MOCK_TRACKS, TracksTable } from '@/features/tracks'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { ReactionButtons } from '@/shared/components'\n\nimport { PageWrapper } from '../common'\nimport s from './PlaylistPage.module.css'\nimport { ControlPanel } from './ui/ControlPanel'\n\nexport const PlaylistPage = () => {\n  const playlist = MOCK_PLAYLIST\n\n  return (\n    <PageWrapper className={s.playlistPage}>\n      <PlaylistOverview\n        className={s.playlistOverview}\n        title={playlist.data.attributes.title}\n        image={playlist.data.attributes.images.main[0].url}\n        description={playlist.data.attributes.description.text}\n        tags={playlist.data.attributes.tags}\n      />\n      <ControlPanel />\n      <TracksTable\n        trackRows={MOCK_TRACKS.map((track, index) => ({\n          index,\n          id: track.id,\n          title: track.attributes.title,\n          image: track.attributes.images.main[0].url,\n          addedAt: track.attributes.addedAt,\n          artists: track.attributes.artists?.map((artist) => artist.name) || [],\n          duration: track.attributes.duration,\n          likesCount: track.attributes.likesCount,\n          dislikesCount: track.attributes.dislikesCount,\n          currentUserReaction: track.attributes.currentUserReaction,\n        }))}\n        renderTrackRow={(trackRow) => (\n          <TrackRow\n            trackRow={trackRow}\n            playingTrackId={MOCK_TRACKS[0].id}\n            playingTrackProgress={20}\n            renderActionsCell={(row) => (\n              <ReactionButtons\n                reaction={row.currentUserReaction}\n                onLike={() => {}}\n                onDislike={() => {}}\n                likesCount={row.likesCount}\n              />\n            )}\n          />\n        )}\n      />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistPage/index.ts",
    "content": "export * from './PlaylistPage'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistPage/ui/ControlPanel/ControlPanel.module.css",
    "content": ".box {\n  display: flex;\n  gap: 24px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.playButton {\n  width: 80px;\n  height: 80px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistPage/ui/ControlPanel/ControlPanel.tsx",
    "content": "import {\n  CurrentUserReaction,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  IconButton,\n  ReactionButtons,\n} from '@/shared/components'\nimport { EditIcon, MoreIcon, PlayIcon } from '@/shared/icons'\n\nimport s from './ControlPanel.module.css'\n\nexport const ControlPanel = () => {\n  return (\n    <div className={s.box}>\n      <IconButton className={s.playButton}>\n        <PlayIcon />\n      </IconButton>\n\n      <ReactionButtons\n        reaction={CurrentUserReaction.None}\n        onLike={() => {}}\n        onDislike={() => {}}\n        size=\"large\"\n      />\n\n      <DropdownMenu>\n        <DropdownMenuTrigger>\n          <MoreIcon />\n        </DropdownMenuTrigger>\n\n        <DropdownMenuContent align=\"start\">\n          <DropdownMenuItem onClick={() => {}}>\n            <EditIcon />\n            <span>Edit</span>\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistPage/ui/ControlPanel/index.ts",
    "content": "export * from './ControlPanel'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistsPage/PlaylistsPage.module.css",
    "content": ".title {\n  margin-bottom: 24px;\n}\n\n.pagination {\n  margin-top: 32px;\n}\n\n.controls {\n  margin-bottom: 32px;\n}\n\n.controlsRow {\n  display: flex;\n  gap: 32px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 32px;\n}\n\n.autocomplete {\n  max-width: 513px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistsPage/PlaylistsPage.tsx",
    "content": "import { useState } from 'react'\n\nimport { MOCK_PLAYLISTS, PlaylistCard } from '@/features/playlists'\nimport { MOCK_HASHTAGS } from '@/features/tags'\nimport { Autocomplete, Pagination, Typography } from '@/shared/components'\n\nimport { ContentList, PageWrapper, SearchTextField, SortSelect } from '../common'\nimport s from './PlaylistsPage.module.css'\n\nexport const PlaylistsPage = () => {\n  const [hashtags, setHashtags] = useState<string[]>([])\n\n  return (\n    <PageWrapper>\n      <Typography variant=\"h2\" as=\"h1\" className={s.title}>\n        All Playlists\n      </Typography>\n      <div className={s.controls}>\n        <div className={s.controlsRow}>\n          <SearchTextField placeholder=\"Search playlists\" onChange={() => {}} />\n          <SortSelect onChange={() => {}} />\n        </div>\n        <Autocomplete\n          options={MOCK_HASHTAGS.map((hashtag) => ({\n            label: hashtag,\n            value: hashtag,\n          }))}\n          value={hashtags}\n          onChange={setHashtags}\n          label=\"Hashtags\"\n          placeholder=\"Search by hashtags\"\n          className={s.autocomplete}\n        />\n      </div>\n      <ContentList\n        data={[...MOCK_PLAYLISTS, ...MOCK_PLAYLISTS, ...MOCK_PLAYLISTS]}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            image={playlist.data.attributes.images.main[0].url}\n            description={playlist.data.attributes.description.text}\n            isShowReactionButtons={true}\n            reaction={playlist.data.attributes.currentUserReaction}\n            onLike={() => {}}\n            onDislike={() => {}}\n            likesCount={playlist.data.attributes.likesCount}\n          />\n        )}\n      />\n      <Pagination className={s.pagination} page={1} pagesCount={10} onPageChange={() => {}} />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/PlaylistsPage/index.ts",
    "content": "export * from './PlaylistsPage'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TrackPage/TrackPage.module.css",
    "content": ".trackPage {\n  --page-gradient-color: #9a3426;\n}\n\n.trackOverview {\n  margin-bottom: 46px;\n}\n\n.title {\n  margin-bottom: 18px;\n}\n\n.search {\n  margin-bottom: 24px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TrackPage/TrackPage.tsx",
    "content": "import { MOCK_PLAYLISTS, PlaylistCard } from '@/features/playlists'\nimport { TrackOverview } from '@/features/tracks'\nimport { Pagination, SearchField, Typography } from '@/shared/components'\n\nimport { ContentList, PageWrapper } from '../common'\nimport s from './TrackPage.module.css'\nimport { ControlPanel } from './ui/ControlPanel'\n\nexport const TrackPage = () => {\n  return (\n    <PageWrapper className={s.trackPage}>\n      <TrackOverview\n        className={s.trackOverview}\n        title=\"Chill Mix\"\n        image=\"https://unsplash.it/297/297\"\n        releaseDate=\"2025-01-01\"\n        artists={['Julia Wolf', 'ayokay', 'Khalid']}\n        tags={['chill', 'mood', 'relax']}\n      />\n\n      <ControlPanel />\n\n      <Typography variant=\"h2\" className={s.title}>\n        In which playlist is the track?\n      </Typography>\n\n      <SearchField placeholder=\"Search playlists\" className={s.search} />\n\n      <ContentList\n        data={[...MOCK_PLAYLISTS]}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            image={playlist.data.attributes.images.main[0].url}\n            description={playlist.data.attributes.description.text}\n          />\n        )}\n      />\n      <Pagination className={s.pagination} page={1} pagesCount={2} onPageChange={() => {}} />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TrackPage/index.ts",
    "content": "export * from './TrackPage'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TrackPage/ui/ControlPanel/ControlPanel.module.css",
    "content": ".box {\n  display: flex;\n  gap: 24px;\n  align-items: center;\n  margin-bottom: 16px;\n}\n\n.playButton {\n  width: 80px;\n  height: 80px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TrackPage/ui/ControlPanel/ControlPanel.tsx",
    "content": "import {\n  CurrentUserReaction,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  IconButton,\n  ReactionButtons,\n} from '@/shared/components'\nimport { AddToPlaylistIcon, EditIcon, MoreIcon, PlayIcon, TextIcon } from '@/shared/icons'\n\nimport s from './ControlPanel.module.css'\n\nexport const ControlPanel = () => {\n  return (\n    <div className={s.box}>\n      <IconButton className={s.playButton}>\n        <PlayIcon />\n      </IconButton>\n\n      <ReactionButtons\n        reaction={CurrentUserReaction.None}\n        onLike={() => {}}\n        onDislike={() => {}}\n        size=\"large\"\n        likesCount={438}\n      />\n\n      <DropdownMenu>\n        <DropdownMenuTrigger>\n          <MoreIcon />\n        </DropdownMenuTrigger>\n\n        <DropdownMenuContent align=\"start\">\n          <DropdownMenuItem onClick={() => {}}>\n            <EditIcon />\n            <span>Edit</span>\n          </DropdownMenuItem>\n\n          <DropdownMenuItem onClick={() => {}}>\n            <AddToPlaylistIcon />\n            <span>Add to playlist</span>\n          </DropdownMenuItem>\n\n          <DropdownMenuItem onClick={() => {}}>\n            <TextIcon />\n            <span>Show lyrics</span>\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TrackPage/ui/ControlPanel/index.ts",
    "content": "export * from './ControlPanel'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TracksPage/TracksPage.module.css",
    "content": ".title {\n  margin-bottom: 24px;\n}\n\n.controls {\n  margin-bottom: 32px;\n}\n\n.controlsRow {\n  display: flex;\n  gap: 32px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 32px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TracksPage/TracksPage.tsx",
    "content": "import { useState } from 'react'\n\nimport { MOCK_ARTISTS } from '@/features/artists/api/artists-api'\nimport { MOCK_HASHTAGS } from '@/features/tags'\nimport { MOCK_TRACKS, TracksTable } from '@/features/tracks'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport {\n  Autocomplete,\n  DropdownMenu,\n  DropdownMenuTrigger,\n  ReactionButtons,\n  Typography,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport { PageWrapper, SearchTextField, SortSelect } from '../common'\nimport s from './TracksPage.module.css'\n\nexport const TracksPage = () => {\n  const [hashtags, setHashtags] = useState<string[]>([])\n  const [artists, setArtists] = useState<string[]>([])\n\n  return (\n    <PageWrapper>\n      <Typography variant=\"h2\" as=\"h1\" className={s.title}>\n        All Tracks\n      </Typography>\n      <div className={s.controls}>\n        <div className={s.controlsRow}>\n          <SearchTextField placeholder=\"Search tracks\" onChange={() => {}} />\n          <SortSelect onChange={() => {}} />\n        </div>\n        <div className={s.controlsRow}>\n          <Autocomplete\n            options={MOCK_HASHTAGS.map((hashtag) => ({\n              label: hashtag,\n              value: hashtag,\n            }))}\n            value={hashtags}\n            onChange={setHashtags}\n            label=\"Hashtags\"\n            placeholder=\"Search by hashtags\"\n            className={s.autocomplete}\n          />\n          <Autocomplete\n            options={MOCK_ARTISTS.map((artist) => ({\n              label: artist.name,\n              value: artist.id,\n            }))}\n            value={artists}\n            onChange={setArtists}\n            label=\"Artists\"\n            placeholder=\"Search by artists\"\n            className={s.autocomplete}\n          />\n        </div>\n      </div>\n\n      <TracksTable\n        trackRows={MOCK_TRACKS.map((track, index) => ({\n          index,\n          id: track.id,\n          title: track.attributes.title,\n          image: track.attributes.images.main[0].url,\n          addedAt: track.attributes.addedAt,\n          artists: track.attributes.artists?.map((artist) => artist.name) || [],\n          duration: track.attributes.duration,\n          likesCount: track.attributes.likesCount,\n          dislikesCount: track.attributes.dislikesCount,\n          currentUserReaction: track.attributes.currentUserReaction,\n        }))}\n        renderTrackRow={(trackRow) => (\n          <TrackRow\n            key={trackRow.id}\n            trackRow={trackRow}\n            playingTrackId={MOCK_TRACKS[0].id}\n            playingTrackProgress={20}\n            renderActionsCell={() => (\n              <>\n                <ReactionButtons\n                  reaction={trackRow.currentUserReaction}\n                  onLike={() => {}}\n                  onDislike={() => {}}\n                  likesCount={trackRow.likesCount}\n                />\n                <DropdownMenu>\n                  <DropdownMenuTrigger>\n                    <MoreIcon />\n                  </DropdownMenuTrigger>\n                </DropdownMenu>\n              </>\n            )}\n          />\n        )}\n      />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/TracksPage/index.ts",
    "content": "export * from './TracksPage'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/UserPage.module.css",
    "content": ".userPage {\n  --page-gradient-color: #b8a661;\n\n  display: flex;\n  flex-direction: column;\n  gap: 24px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/UserPage.tsx",
    "content": "import { PageWrapper } from '../common'\nimport { UserInfo, UserTabs } from './ui'\nimport s from './UserPage.module.css'\n\nexport const UserPage = () => {\n  return (\n    <PageWrapper className={s.userPage}>\n      <UserInfo />\n      <UserTabs />\n    </PageWrapper>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/index.ts",
    "content": "export * from './UserPage'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserInfo/UserInfo.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  align-items: center;\n}\n\n.avatar {\n  overflow: hidden;\n  width: 192px;\n  height: 192px;\n  border-radius: 50%;\n}\n\n.descriptionList {\n  display: flex;\n  gap: 23px;\n  margin: 0;\n}\n\n.descriptionItem {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.descriptionItem dd {\n  margin: 0;\n}\n\n.descriptionItem dt {\n  font-size: var(--font-size-s);\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserInfo/UserInfo.tsx",
    "content": "import { Button, Typography } from '@/shared/components'\nimport { EditIcon } from '@/shared/icons'\n\nimport s from './UserInfo.module.css'\n\nexport const UserInfo = () => {\n  return (\n    <div className={s.box}>\n      <div className={s.avatar}>\n        <img src={'https://unsplash.it/192/192'} alt=\"User avatar\" />\n      </div>\n      <Typography variant=\"h2\">Martin Fowler</Typography>\n\n      <Button variant=\"secondary\">\n        <EditIcon /> Edit profile\n      </Button>\n      <dl className={s.descriptionList}>\n        <div className={s.descriptionItem}>\n          <Typography as=\"dd\" variant=\"body1\">\n            58\n          </Typography>\n          <Typography as=\"dt\" variant=\"body2\">\n            Playlists\n          </Typography>\n        </div>\n        <div className={s.descriptionItem}>\n          <Typography as=\"dd\" variant=\"body1\">\n            100\n          </Typography>\n          <Typography as=\"dt\" variant=\"body2\">\n            Tracks\n          </Typography>\n        </div>\n      </dl>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserInfo/index.ts",
    "content": "export * from './UserInfo'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/LikedTracksTab/LikedTracksTab.module.css",
    "content": ""
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/LikedTracksTab/LikedTracksTab.tsx",
    "content": "import { MOCK_TRACKS } from '@/features/tracks'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { TracksTable } from '@/features/tracks/ui/TracksTable/TracksTable'\nimport { ReactionButtons } from '@/shared/components'\nimport { DropdownMenu, DropdownMenuTrigger } from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nexport const LikedTracksTab = () => {\n  return (\n    <TracksTable\n      trackRows={MOCK_TRACKS.map((track, index) => ({\n        index,\n        id: track.id,\n        title: track.attributes.title,\n        image: track.attributes.images.main[0].url,\n        addedAt: track.attributes.addedAt,\n        artists: track.attributes.artists?.map((artist) => artist.name) || [],\n        duration: track.attributes.duration,\n        likesCount: track.attributes.likesCount,\n        dislikesCount: track.attributes.dislikesCount,\n        currentUserReaction: track.attributes.currentUserReaction,\n      }))}\n      renderTrackRow={(trackRow) => (\n        <TrackRow\n          trackRow={trackRow}\n          playingTrackId={MOCK_TRACKS[0].id}\n          playingTrackProgress={20}\n          renderActionsCell={() => (\n            <>\n              <ReactionButtons\n                reaction={trackRow.currentUserReaction}\n                onLike={() => {}}\n                onDislike={() => {}}\n                likesCount={trackRow.likesCount}\n              />\n              <DropdownMenu>\n                <DropdownMenuTrigger>\n                  <MoreIcon />\n                </DropdownMenuTrigger>\n              </DropdownMenu>\n            </>\n          )}\n        />\n      )}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/LikedTracksTab/index.ts",
    "content": "export * from './LikedTracksTab'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/MyLikedPlaylistsTab.tsx",
    "content": "import { MOCK_PLAYLISTS, PlaylistCard } from '@/features/playlists'\nimport { ContentList } from '@/pages/common'\nimport { Pagination } from '@/shared/components'\n\nexport const MyLikedPlaylistsTab = () => {\n  return (\n    <>\n      <ContentList\n        data={[...MOCK_PLAYLISTS]}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            image={playlist.data.attributes.images.main[0].url}\n            description={playlist.data.attributes.description.text}\n            likesCount={playlist.data.attributes.likesCount}\n            isShowReactionButtons={true}\n            reaction={playlist.data.attributes.currentUserReaction}\n            onLike={() => {}}\n            onDislike={() => {}}\n          />\n        )}\n      />\n      <Pagination page={1} pagesCount={2} onPageChange={() => {}} />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/MyLikedPlaylistsTab/index.ts",
    "content": "export * from './MyLikedPlaylistsTab'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/PlaylistsTab/PlaylistsTab.module.css",
    "content": ".createPlaylistButton {\n  display: block;\n\n  width: 328px;\n  height: 54px;\n  margin: 0 auto;\n  margin-bottom: 24px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/PlaylistsTab/PlaylistsTab.tsx",
    "content": "import { useState } from 'react'\n\nimport { CreatePlaylistModal, MOCK_PLAYLISTS, PlaylistCard } from '@/features/playlists'\nimport { ContentList } from '@/pages/common'\nimport { Button, Pagination, Typography } from '@/shared/components'\n\nimport s from './PlaylistsTab.module.css'\n\nexport const PlaylistsTab = () => {\n  const [isCreatePlaylistModalOpen, setIsCreatePlaylistModalOpen] = useState(false) // STATE FOR TESTING\n\n  const openCreatePlaylistModal = () => {\n    setIsCreatePlaylistModalOpen(true)\n  }\n\n  return (\n    <>\n      <Button className={s.createPlaylistButton} onClick={openCreatePlaylistModal}>\n        Create Playlist\n      </Button>\n\n      {isCreatePlaylistModalOpen && (\n        <CreatePlaylistModal onClose={() => setIsCreatePlaylistModalOpen(false)} />\n      )}\n      <ContentList\n        data={[...MOCK_PLAYLISTS]}\n        renderItem={(playlist) => (\n          <PlaylistCard\n            id={playlist.data.id}\n            title={playlist.data.attributes.title}\n            image={playlist.data.attributes.images.main[0].url}\n            description={playlist.data.attributes.description.text}\n          />\n        )}\n      />\n      <Pagination page={1} pagesCount={2} onPageChange={() => {}} />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/PlaylistsTab/index.ts",
    "content": "export * from './PlaylistsTab'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/TracksTab/TracksTab.module.css",
    "content": ".uploadTrackButton {\n  display: block;\n\n  width: 328px;\n  height: 54px;\n  margin: 0 auto;\n  margin-bottom: 24px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/TracksTab/TracksTab.tsx",
    "content": "// import { useState } from 'react'\n\nimport { MOCK_TRACKS, TracksTable } from '@/features/tracks'\nimport { TrackRow } from '@/features/tracks/ui/TrackRow/TrackRow'\nimport { Button } from '@/shared/components'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/shared/components'\nimport { MoreIcon } from '@/shared/icons'\n\nimport s from './TracksTab.module.css'\n\nexport const TracksTab = () => {\n  // const [isUploadTrackModalOpen, setIsUploadTrackModalOpen] = useState(false) // STATE FOR TESTING\n\n  const openUploadTrackModal = () => {\n    // setIsUploadTrackModalOpen(true)\n  }\n\n  return (\n    <>\n      <Button className={s.uploadTrackButton} onClick={openUploadTrackModal}>\n        Upload Track\n      </Button>\n      <TracksTable\n        trackRows={MOCK_TRACKS.map((track, index) => ({\n          index,\n          id: track.id,\n          title: track.attributes.title,\n          image: track.attributes.images.main[0].url,\n          addedAt: track.attributes.addedAt,\n          artists: track.attributes.artists?.map((artist) => artist.name) || [],\n          duration: track.attributes.duration,\n        }))}\n        renderTrackRow={(trackRow) => (\n          <TrackRow\n            trackRow={trackRow}\n            renderActionsCell={() => (\n              <DropdownMenu>\n                <DropdownMenuTrigger>\n                  <MoreIcon />\n                </DropdownMenuTrigger>\n                <DropdownMenuContent>\n                  <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n                  <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n                    Add to playlist\n                  </DropdownMenuItem>\n                  <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n                    Show text song\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n            )}\n          />\n        )}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/TracksTab/index.ts",
    "content": ""
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/UserTabs.tsx",
    "content": "import { Tabs, TabsContent, TabsList, TabsTrigger, Typography } from '@/shared/components'\n\nimport { LikedTracksTab } from './LikedTracksTab'\nimport { MyLikedPlaylistsTab } from './MyLikedPlaylistsTab'\nimport { PlaylistsTab } from './PlaylistsTab'\nimport { TracksTab } from './TracksTab/TracksTab'\n\nexport const UserTabs = () => {\n  const isProfileOwner = true // STATE FOR TESTING\n\n  return (\n    <Tabs defaultValue=\"playlists\">\n      <TabsList>\n        <TabsTrigger value=\"playlists\">Playlists</TabsTrigger>\n        <TabsTrigger value=\"tracks\">Tracks</TabsTrigger>\n        {isProfileOwner && (\n          <>\n            <TabsTrigger value=\"liked-playlists\">Liked Playlists</TabsTrigger>\n            <TabsTrigger value=\"liked-tracks\">Liked Tracks</TabsTrigger>\n          </>\n        )}\n      </TabsList>\n      <TabsContent value=\"playlists\">\n        <PlaylistsTab />\n      </TabsContent>\n      <TabsContent value=\"tracks\">\n        <TracksTab />\n      </TabsContent>\n      {isProfileOwner && (\n        <>\n          <TabsContent value=\"liked-playlists\">\n            <MyLikedPlaylistsTab />\n          </TabsContent>\n          <TabsContent value=\"liked-tracks\">\n            <LikedTracksTab />\n          </TabsContent>\n        </>\n      )}\n    </Tabs>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/UserTabs/index.ts",
    "content": "export * from './UserTabs'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/UserPage/ui/index.ts",
    "content": "export * from './UserInfo'\nexport * from './UserTabs'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/ContentList/ContentList.module.css",
    "content": ".title {\n  margin-bottom: 20px;\n}\n\n.list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--list-gap, 8px);\n  padding-bottom: 8px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/ContentList/ContentList.tsx",
    "content": "import clsx from 'clsx'\n\nimport { Typography } from '@/shared/components/Typography/Typography'\n\nimport s from './ContentList.module.css'\n\ntype ContentListProps<T> = {\n  title?: string\n  data: T[]\n  renderItem: (item: T) => React.ReactNode\n  listClassName?: string\n}\n\nexport const ContentList = <T,>({\n  title,\n  data,\n  renderItem,\n  listClassName,\n}: ContentListProps<T>) => {\n  return (\n    <section>\n      {title && (\n        <Typography variant=\"h2\" className={s.title}>\n          {title}\n        </Typography>\n      )}\n      <ul className={clsx(s.list, listClassName)}>\n        {data.map((item, index) => (\n          <li key={index}>{renderItem(item)}</li>\n        ))}\n      </ul>\n    </section>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/ContentList/index.ts",
    "content": "export * from './ContentList'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/PageWrapper/PageWrapper.module.css",
    "content": ".wrapper {\n  min-height: calc(100vh - var(--header-height));\n  padding: 30px 40px;\n  background: linear-gradient(\n    180deg,\n    var(--page-gradient-color, #3333a3) 0,\n    var(--color-bg-secondary) 300px,\n    var(--color-bg-secondary) 100%\n  );\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/PageWrapper/PageWrapper.tsx",
    "content": "import clsx from 'clsx'\n\nimport s from './PageWrapper.module.css'\n\ntype PageWrapperProps = {\n  children: React.ReactNode\n  className?: string\n}\n\nexport const PageWrapper = ({ children, className }: PageWrapperProps) => {\n  return <div className={clsx(s.wrapper, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/PageWrapper/index.ts",
    "content": "export * from './PageWrapper'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/SearchTextField/SearchTextField.tsx",
    "content": "import { TextField, type TextFieldProps } from '@/shared/components'\nimport { SearchIcon } from '@/shared/icons'\n\nexport const SearchTextField = (props: Omit<TextFieldProps, 'icon' | 'inputSize'>) => {\n  return (\n    <TextField\n      {...props}\n      icon={<SearchIcon width={20} height={20} />}\n      inputSize=\"l\"\n      autoComplete=\"off\"\n    />\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/SearchTextField/index.ts",
    "content": "export * from './SearchTextField'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/SortSelect/SortSelect.module.css",
    "content": ".selectLabel {\n  display: flex;\n  flex-shrink: 0;\n  gap: 8px;\n  align-items: center;\n\n  width: 210px;\n}\n\n.select {\n  flex-shrink: 0;\n  width: 145px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/SortSelect/SortSelect.tsx",
    "content": "import { Select, type SelectProps } from '@/shared/components'\n\nimport s from './SortSelect.module.css'\n\nexport const SortSelect = (props: Omit<SelectProps, 'options'>) => {\n  return (\n    <label className={s.selectLabel}>\n      Sort By\n      <Select\n        {...props}\n        options={[\n          { value: 'newest', label: 'Newest first' },\n          { value: 'oldest', label: 'Oldest first' },\n          { value: 'mostLiked', label: 'Most liked' },\n          { value: 'leastLiked', label: 'Least liked' },\n        ]}\n        className={s.select}\n      />\n    </label>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/SortSelect/index.ts",
    "content": "export * from './SortSelect'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/common/index.ts",
    "content": "export * from './ContentList'\nexport * from './PageWrapper'\nexport * from './SearchTextField'\nexport * from './SortSelect'\n"
  },
  {
    "path": "apps/ui-vanilla/src/pages/index.ts",
    "content": "export * from './MainPage'\nexport * from './PlaylistPage'\nexport * from './PlaylistsPage'\nexport * from './TrackPage'\nexport * from './TracksPage'\nexport * from './UserPage'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/AudioPlayer/AudioPlayer.module.css",
    "content": ".player {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  width: 100%;\n  min-height: 64px;\n\n  background: var(--color-bg-primary);\n}\n\n.trackInfo {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  min-width: 200px;\n}\n\n.cover {\n  width: 112px;\n  height: 112px;\n  border-radius: 4px;\n  background: var(--color-bg-card);\n}\n\n.cover img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.info {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.playerControls {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 8px;\n  align-items: center;\n}\n\n.controls {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n}\n\n.playPauseButton {\n  width: 48px;\n  height: 48px;\n}\n\n.active {\n  color: var(--color-accent);\n}\n\n.iconButton.active:hover,\n.iconButton.active:focus {\n  color: var(--color-accent);\n}\n\n.progressBar {\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  max-width: 632px;\n}\n\n.time {\n  min-width: 36px;\n  font-size: var(--font-size-xs);\n  color: var(--color-text-secondary);\n  text-align: center;\n}\n\n.progress {\n  cursor: pointer;\n\n  height: 5px;\n  border: none;\n  border-radius: 4px;\n\n  accent-color: var(--color-text-primary);\n}\n\n.trackProgress {\n  width: 100%;\n  max-width: 550px;\n}\n\n.volumeColumn {\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n\n  min-width: 160px;\n  padding-right: 32px;\n}\n\n.volumeProgress {\n  width: 119px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/AudioPlayer/AudioPlayer.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { AudioPlayer } from './AudioPlayer.tsx'\n\nconst meta = {\n  title: 'Components/Player',\n  component: AudioPlayer,\n  parameters: {},\n  args: {},\n} satisfies Meta<typeof AudioPlayer>\n\nexport default meta\n\nconst demoTrack = {\n  src: 'https://cdn.uppbeat.io/audio-files/c636d7c86452449b1203fc0bded83e29/4358717fc9da477a52fb18a6cbd3afcc/d154b5ce5ff1a05ae8115a3c678062e8/STREAMING-dreamland-matrika-main-version-31140-02-25.mp3',\n  cover: 'https://unsplash.it/112/112',\n  title: 'Play It Safe',\n  artist: 'Julia Wolf',\n}\n\nexport const Basic = {\n  render: () => {\n    const [isPlaying, setIsPlaying] = useState(false)\n    const [isShuffle, setIsShuffle] = useState(false)\n    const [isRepeat, setIsRepeat] = useState(false)\n\n    const [track] = useState(demoTrack)\n    return (\n      <AudioPlayer\n        {...track}\n        isPlaying={isPlaying}\n        setIsPlaying={setIsPlaying}\n        onNext={() => {}}\n        onPrevious={() => {}}\n        isShuffle={isShuffle}\n        isRepeat={isRepeat}\n        onShuffle={() => setIsShuffle(!isShuffle)}\n        onRepeat={() => setIsRepeat(!isRepeat)}\n      />\n    )\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/AudioPlayer/AudioPlayer.tsx",
    "content": "import { clsx } from 'clsx'\nimport { ComponentProps, useRef, useState } from 'react'\n\nimport {\n  PauseIcon,\n  PlayIcon,\n  RepeatIcon,\n  ShuffleIcon,\n  SkipNextIcon,\n  SkipPreviousIcon,\n  VolumeIcon,\n  VolumeMuteIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './AudioPlayer.module.css'\n\nexport type PlayerProps = {\n  src: string\n  cover: string\n  title: string\n  artist: string\n  isPlaying: boolean\n  setIsPlaying: (isPlaying: boolean) => void\n  onNext: () => void\n  onPrevious: () => void\n  isShuffle: boolean\n  isRepeat: boolean\n  onShuffle: () => void\n  onRepeat: () => void\n} & ComponentProps<'div'>\n\nexport const AudioPlayer = ({\n  src,\n  cover,\n  title,\n  artist,\n  isPlaying,\n  setIsPlaying,\n  onNext,\n  onPrevious,\n  isShuffle,\n  isRepeat,\n  onShuffle,\n  onRepeat,\n  className,\n  ...props\n}: PlayerProps) => {\n  const audioRef = useRef<HTMLAudioElement | null>(null)\n  const [currentTime, setCurrentTime] = useState(0)\n  const [volume, setVolume] = useState(1)\n  const [duration, setDuration] = useState(0)\n\n  const handlePlayPause = () => {\n    const audio = audioRef.current\n    if (!audio) return\n\n    if (isPlaying) {\n      audio.pause()\n    } else {\n      audio.play().catch((e) => {\n        console.error('Audio play error:', e)\n      })\n    }\n\n    setIsPlaying(!isPlaying)\n  }\n\n  const handleChangeTime = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const time = Number(e.target.value)\n    setCurrentTime(time)\n    if (audioRef.current) {\n      audioRef.current.currentTime = time\n    }\n  }\n\n  const handleVolume = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newVolume = Number(e.target.value)\n    setVolume(newVolume)\n    if (audioRef.current) {\n      audioRef.current.volume = newVolume\n    }\n  }\n\n  const handleVolumeMute = () => {\n    const newVolume = volume > 0 ? 0 : 1\n    setVolume(newVolume)\n    if (audioRef.current) {\n      audioRef.current.volume = newVolume\n    }\n  }\n\n  return (\n    <div className={clsx(s.player, className)} {...props}>\n      <audio\n        ref={audioRef}\n        src={src}\n        onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}\n        onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}\n      />\n\n      <div className={s.trackInfo}>\n        <div className={s.cover}>\n          <img src={cover} alt=\"cover\" />\n        </div>\n        <div className={s.info}>\n          <Typography variant=\"body1\" as=\"h3\">\n            {title}\n          </Typography>\n          <Typography variant=\"body2\" as=\"p\">\n            {artist}\n          </Typography>\n        </div>\n      </div>\n\n      <div className={s.playerControls}>\n        <div className={s.controls}>\n          <IconButton onClick={onShuffle} className={clsx(s.iconButton, isShuffle && s.active)}>\n            <ShuffleIcon />\n          </IconButton>\n          <IconButton onClick={onPrevious}>\n            <SkipPreviousIcon />\n          </IconButton>\n          <IconButton className={s.playPauseButton} onClick={handlePlayPause}>\n            {isPlaying ? <PauseIcon /> : <PlayIcon />}\n          </IconButton>\n          <IconButton onClick={onNext}>\n            <SkipNextIcon />\n          </IconButton>\n          <IconButton onClick={onRepeat} className={clsx(s.iconButton, isRepeat && s.active)}>\n            <RepeatIcon />\n          </IconButton>\n        </div>\n\n        <div className={s.progressBar}>\n          <span className={s.time}>{format(currentTime)}</span>\n          <input\n            type=\"range\"\n            min={0}\n            max={duration}\n            value={currentTime}\n            onChange={handleChangeTime}\n            className={clsx(s.progress, s.trackProgress)}\n          />\n          <span className={s.time}>{format(duration)}</span>\n        </div>\n      </div>\n\n      <div className={s.volumeColumn}>\n        <IconButton onClick={handleVolumeMute}>\n          {volume > 0 ? <VolumeIcon /> : <VolumeMuteIcon />}\n        </IconButton>\n        <input\n          type=\"range\"\n          min={0}\n          max={1}\n          step={0.01}\n          value={volume}\n          onChange={handleVolume}\n          className={clsx(s.progress, s.volumeProgress)}\n        />\n      </div>\n    </div>\n  )\n}\n\nconst format = (sec: number) => {\n  const m = Math.floor(sec / 60)\n  const s = Math.floor(sec % 60)\n  return `${m}:${s.toString().padStart(2, '0')}`\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/AudioPlayer/index.ts",
    "content": "export * from './AudioPlayer.tsx'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Autocomplete/Autocomplete.module.css",
    "content": ".container {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.labelError {\n  color: var(--color-text-error);\n}\n\n.inputWrapper {\n  position: relative;\n\n  display: flex;\n  flex-wrap: wrap;\n  gap: 6px;\n  align-items: center;\n\n  min-height: 48px;\n  padding: 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.inputWrapper:hover:not(.disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.inputWrapper.focused {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-primary);\n}\n\n.inputWrapper.error {\n  border-color: var(--color-text-error);\n}\n\n.inputWrapper.disabled {\n  cursor: not-allowed;\n  background-color: var(--color-disabled);\n}\n\n.tag {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n\n  padding: 2px 6px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 16px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.inputContainer {\n  position: relative;\n\n  display: flex;\n  flex: 1;\n  align-items: center;\n\n  min-width: 120px;\n}\n\n.searchIcon {\n  pointer-events: none;\n\n  position: absolute;\n  z-index: 1;\n  left: 4px;\n\n  width: 16px;\n  height: 16px;\n\n  color: var(--color-text-secondary);\n\n  transition: color 200ms ease;\n}\n\n.input {\n  width: 100%;\n  padding: 4px 8px 4px 24px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background: transparent;\n  outline: none;\n\n  transition: all 200ms ease;\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.dropdownIcon {\n  cursor: pointer;\n\n  width: 20px;\n  height: 20px;\n  margin-left: 4px;\n\n  color: var(--color-text-secondary);\n\n  transition: transform 200ms ease;\n}\n\n.dropdownIcon:hover {\n  color: var(--color-text-primary);\n}\n\n.dropdownIconOpen {\n  transform: rotate(180deg);\n}\n\n.dropdown {\n  position: absolute;\n  z-index: 50;\n  top: 100%;\n  left: 0;\n\n  overflow-y: auto;\n\n  width: 100%;\n  max-height: 200px;\n  margin-top: 4px;\n  padding: 4px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 4px;\n\n  background-color: var(--color-bg-primary);\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n\n  animation: dropdown-show 200ms ease-out;\n}\n\n.option {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n\n  padding: 8px 12px;\n  border-radius: 4px;\n\n  transition: all 200ms ease;\n}\n\n.option:hover:not(.optionDisabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.optionFocused:not(.optionDisabled) {\n  color: var(--color-bg-primary);\n  background-color: var(--color-accent);\n}\n\n.optionDisabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.noResults {\n  padding: 12px;\n  text-align: center;\n}\n\n.noResultsText {\n  color: var(--color-text-secondary);\n}\n\n.errorMessage {\n  margin-top: 4px;\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.counter {\n  margin-top: 4px;\n  color: var(--color-text-secondary);\n}\n\n/* Animations */\n@keyframes dropdown-show {\n  from {\n    transform: translateY(-4px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Autocomplete/Autocomplete.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from '../Dialog'\nimport { Typography } from '../Typography'\nimport { Autocomplete, type AutocompleteOption } from './Autocomplete'\n\nconst meta = {\n  title: 'Components/Autocomplete',\n  component: Autocomplete,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Autocomplete>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n// Sample data\nconst programmingLanguages: AutocompleteOption[] = [\n  { value: 'javascript', label: 'JavaScript' },\n  { value: 'typescript', label: 'TypeScript' },\n  { value: 'python', label: 'Python' },\n  { value: 'java', label: 'Java' },\n  { value: 'cpp', label: 'C++' },\n  { value: 'csharp', label: 'C#' },\n  { value: 'php', label: 'PHP' },\n  { value: 'ruby', label: 'Ruby' },\n  { value: 'go', label: 'Go' },\n  { value: 'rust', label: 'Rust' },\n  { value: 'kotlin', label: 'Kotlin' },\n  { value: 'swift', label: 'Swift' },\n]\n\nconst musicGenres: AutocompleteOption[] = [\n  { value: 'rock', label: 'Rock' },\n  { value: 'pop', label: 'Pop' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hiphop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n  { value: 'blues', label: 'Blues' },\n  { value: 'reggae', label: 'Reggae' },\n  { value: 'folk', label: 'Folk' },\n  { value: 'metal', label: 'Metal' },\n  { value: 'indie', label: 'Indie' },\n]\n\nconst skills: AutocompleteOption[] = [\n  { value: 'frontend', label: 'Frontend Development' },\n  { value: 'backend', label: 'Backend Development' },\n  { value: 'fullstack', label: 'Full Stack Development' },\n  { value: 'mobile', label: 'Mobile Development' },\n  { value: 'devops', label: 'DevOps' },\n  { value: 'testing', label: 'Testing & QA' },\n  { value: 'design', label: 'UI/UX Design' },\n  { value: 'pm', label: 'Project Management', disabled: true },\n  { value: 'data', label: 'Data Science' },\n  { value: 'ml', label: 'Machine Learning' },\n]\n\nexport const Basic = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Programming Languages\"\n          placeholder=\"Search and select languages...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (max 3)\"\n          placeholder=\"Choose up to 3 genres...\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          maxTags={3}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithPreselected = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>(['javascript', 'typescript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Your Skills\"\n          placeholder=\"Add more skills...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithDisabledOptions = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Skills & Roles\"\n          placeholder=\"Select your skills...\"\n          options={skills}\n          value={selectedValues}\n          onChange={setSelectedValues}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>(['rock', 'jazz'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Music Genres (disabled)\"\n          placeholder=\"Cannot select\"\n          options={musicGenres}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          disabled\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithError = {\n  render: () => {\n    const [selectedValues, setSelectedValues] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Autocomplete\n          label=\"Required Skills\"\n          placeholder=\"Select at least one skill...\"\n          options={programmingLanguages}\n          value={selectedValues}\n          onChange={setSelectedValues}\n          errorMessage=\"Please select at least one programming language\"\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendSkills, setFrontendSkills] = useState<string[]>(['javascript'])\n    const [backendSkills, setBackendSkills] = useState<string[]>([])\n    const [genres, setGenres] = useState<string[]>([])\n\n    const frontendOptions: AutocompleteOption[] = [\n      { value: 'html', label: 'HTML' },\n      { value: 'css', label: 'CSS' },\n      { value: 'javascript', label: 'JavaScript' },\n      { value: 'typescript', label: 'TypeScript' },\n      { value: 'react', label: 'React' },\n      { value: 'vue', label: 'Vue.js' },\n      { value: 'angular', label: 'Angular' },\n      { value: 'svelte', label: 'Svelte' },\n    ]\n\n    const backendOptions: AutocompleteOption[] = [\n      { value: 'nodejs', label: 'Node.js' },\n      { value: 'python', label: 'Python' },\n      { value: 'java', label: 'Java' },\n      { value: 'csharp', label: 'C#' },\n      { value: 'php', label: 'PHP' },\n      { value: 'ruby', label: 'Ruby' },\n      { value: 'go', label: 'Go' },\n      { value: 'rust', label: 'Rust' },\n    ]\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Developer Profile Setup\n          </Typography>\n        </div>\n\n        <Autocomplete\n          label=\"Frontend Technologies\"\n          placeholder=\"Select frontend skills...\"\n          options={frontendOptions}\n          value={frontendSkills}\n          onChange={setFrontendSkills}\n          maxTags={5}\n        />\n\n        <Autocomplete\n          label=\"Backend Technologies\"\n          placeholder=\"Select backend skills...\"\n          options={backendOptions}\n          value={backendSkills}\n          onChange={setBackendSkills}\n          maxTags={4}\n        />\n\n        <Autocomplete\n          label=\"Favorite Music Genres\"\n          placeholder=\"What music do you like?\"\n          options={musicGenres}\n          value={genres}\n          onChange={setGenres}\n          maxTags={6}\n        />\n\n        <Card style={{ padding: '16px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Profile Summary\n          </Typography>\n\n          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>\n            <Typography variant=\"body2\">\n              <strong>Frontend:</strong>{' '}\n              {frontendSkills.length > 0 ? frontendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Backend:</strong>{' '}\n              {backendSkills.length > 0 ? backendSkills.join(', ') : 'None'}\n            </Typography>\n            <Typography variant=\"body2\">\n              <strong>Music:</strong> {genres.length > 0 ? genres.join(', ') : 'None'}\n            </Typography>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => {\n    const [state1, setState1] = useState<string[]>([])\n    const [state2, setState2] = useState<string[]>(['rock', 'jazz'])\n    const [state3, setState3] = useState<string[]>([])\n    const [state4, setState4] = useState<string[]>(['javascript'])\n\n    return (\n      <div\n        style={{\n          width: '600px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '32px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Empty State\n          </Typography>\n          <Autocomplete\n            label=\"Programming Languages\"\n            placeholder=\"Start typing to search...\"\n            options={programmingLanguages}\n            value={state1}\n            onChange={setState1}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Selected Values\n          </Typography>\n          <Autocomplete\n            label=\"Music Genres\"\n            placeholder=\"Add more genres...\"\n            options={musicGenres}\n            value={state2}\n            onChange={setState2}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            With Error\n          </Typography>\n          <Autocomplete\n            label=\"Required Field\"\n            placeholder=\"This field is required\"\n            options={programmingLanguages}\n            value={state3}\n            onChange={setState3}\n            errorMessage=\"Please select at least one option\"\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Disabled State\n          </Typography>\n          <Autocomplete\n            label=\"Locked Selection\"\n            placeholder=\"Cannot modify\"\n            options={programmingLanguages}\n            value={state4}\n            onChange={setState4}\n            disabled\n          />\n        </div>\n      </div>\n    )\n  },\n}\n\nexport const InDialog = {\n  render: () => {\n    const [isOpen, setIsOpen] = useState(false)\n    const [selectedSkills, setSelectedSkills] = useState<string[]>([])\n    const [selectedGenres, setSelectedGenres] = useState<string[]>(['rock'])\n\n    const handleSubmit = () => {\n      console.log('Selected skills:', selectedSkills)\n      console.log('Selected genres:', selectedGenres)\n      setIsOpen(false)\n    }\n\n    const handleReset = () => {\n      setSelectedSkills([])\n      setSelectedGenres([])\n    }\n\n    return (\n      <>\n        <Button onClick={() => setIsOpen(true)}>Open Profile Settings</Button>\n\n        <Dialog open={isOpen} onClose={() => setIsOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Edit Your Profile</Typography>\n            <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n              Update your skills and music preferences\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '24px',\n                minWidth: '400px',\n              }}>\n              <Autocomplete\n                label=\"Technical Skills\"\n                placeholder=\"Search and select your skills...\"\n                options={skills}\n                value={selectedSkills}\n                onChange={setSelectedSkills}\n                maxTags={8}\n              />\n\n              <Autocomplete\n                label=\"Favorite Music Genres\"\n                placeholder=\"What music do you enjoy?\"\n                options={musicGenres}\n                value={selectedGenres}\n                onChange={setSelectedGenres}\n                maxTags={5}\n              />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset All\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setIsOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={handleSubmit}>\n              Save Profile\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Autocomplete/Autocomplete.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\n\nimport { useGetId } from '@/shared/hooks'\nimport { ArrowDownIcon, DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './Autocomplete.module.css'\n\nexport type AutocompleteOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type AutocompleteProps = {\n  label?: ReactNode\n  placeholder?: string\n  options: AutocompleteOption[]\n  value: string[]\n  onChange: (value: string[]) => void\n  disabled?: boolean\n  maxTags?: number\n  errorMessage?: string\n  className?: string\n} & Omit<ComponentProps<'div'>, 'onChange'>\n\nexport const Autocomplete = ({\n  label,\n  placeholder = 'Search and select...',\n  options,\n  value,\n  onChange,\n  disabled = false,\n  maxTags,\n  errorMessage,\n  className,\n  ...props\n}: AutocompleteProps) => {\n  const [isOpen, setIsOpen] = useState(false)\n  const [searchTerm, setSearchTerm] = useState('')\n  const [focusedIndex, setFocusedIndex] = useState(-1)\n\n  // For detecting clicks outside component to close dropdown\n  const containerRef = useRef<HTMLDivElement>(null)\n  // For programmatic focus management (Escape key, focus after selection)\n  const inputRef = useRef<HTMLInputElement>(null)\n\n  const id = useGetId(props.id)\n\n  const filteredOptions = options.filter(\n    (option) =>\n      option.label.toLowerCase().includes(searchTerm.toLowerCase()) && !value.includes(option.value)\n  )\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n  const showError = Boolean(errorMessage)\n\n  // Close dropdown on outside click\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n        setIsOpen(false)\n        setFocusedIndex(-1)\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen])\n\n  // Handle keyboard navigation\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (disabled) return\n\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault()\n        if (!isOpen) {\n          setIsOpen(true)\n          setFocusedIndex(0)\n        } else {\n          setFocusedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : prev))\n        }\n        break\n\n      case 'ArrowUp':\n        e.preventDefault()\n        setFocusedIndex((prev) => (prev > 0 ? prev - 1 : 0))\n        break\n\n      case 'Enter':\n        e.preventDefault()\n        if (isOpen && focusedIndex >= 0 && filteredOptions[focusedIndex]) {\n          selectOption(filteredOptions[focusedIndex])\n        }\n        break\n\n      case 'Escape':\n        e.preventDefault()\n        setIsOpen(false)\n        setFocusedIndex(-1)\n        inputRef.current?.blur()\n        break\n\n      case 'Backspace':\n        if (!searchTerm && value.length > 0) {\n          removeTag(value[value.length - 1])\n        }\n        break\n    }\n  }\n\n  const selectOption = (option: AutocompleteOption) => {\n    if (option.disabled || isMaxTagsReached) return\n\n    onChange([...value, option.value])\n    setSearchTerm('')\n    setFocusedIndex(-1)\n    inputRef.current?.focus()\n  }\n\n  const removeTag = (tagValue: string) => {\n    onChange(value.filter((v) => v !== tagValue))\n  }\n\n  const handleInputFocus = () => {\n    if (!disabled) {\n      setIsOpen(true)\n    }\n  }\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setSearchTerm(e.target.value)\n    setIsOpen(true)\n    setFocusedIndex(-1)\n  }\n\n  const selectedOptions = options.filter((option) => value.includes(option.value))\n\n  return (\n    <div className={clsx(s.container, className)} ref={containerRef} {...props}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          className={clsx(s.label, showError && s.labelError)}\n          as=\"label\"\n          htmlFor={id}>\n          {label}\n        </Typography>\n      )}\n\n      <div\n        className={clsx(\n          s.inputWrapper,\n          isOpen && s.focused,\n          showError && s.error,\n          disabled && s.disabled\n        )}>\n        {/* Selected tags */}\n        {selectedOptions.map((option) => (\n          <div key={option.value} className={s.tag}>\n            <Typography variant=\"body2\" className={s.tagText} as=\"label\">\n              {option.label}\n            </Typography>\n            {!disabled && (\n              <IconButton\n                onClick={() => removeTag(option.value)}\n                className={s.deleteButton}\n                aria-label={`Remove ${option.label}`}\n                type=\"button\"\n                tabIndex={-1}>\n                <DeleteIcon />\n              </IconButton>\n            )}\n          </div>\n        ))}\n\n        {/* Search input */}\n        <div className={s.inputContainer}>\n          <input\n            id={id}\n            ref={inputRef}\n            type=\"text\"\n            className={s.input}\n            value={searchTerm}\n            onChange={handleInputChange}\n            onFocus={handleInputFocus}\n            onKeyDown={handleKeyDown}\n            placeholder={value.length === 0 ? placeholder : ''}\n            disabled={disabled || isMaxTagsReached}\n            autoComplete=\"off\"\n          />\n        </div>\n\n        {/* Dropdown arrow */}\n        <ArrowDownIcon\n          className={clsx(s.dropdownIcon, isOpen && s.dropdownIconOpen)}\n          onClick={() => !disabled && setIsOpen(!isOpen)}\n        />\n      </div>\n\n      {/* Dropdown */}\n      {isOpen && !disabled && (\n        <div className={s.dropdown}>\n          {filteredOptions.length > 0 ? (\n            filteredOptions.map((option, index) => (\n              <div\n                key={option.value}\n                className={clsx(\n                  s.option,\n                  index === focusedIndex && s.optionFocused,\n                  option.disabled && s.optionDisabled\n                )}\n                onClick={() => !option.disabled && selectOption(option)}\n                onMouseEnter={() => setFocusedIndex(index)}>\n                <Typography variant=\"body2\">{option.label}</Typography>\n              </div>\n            ))\n          ) : (\n            <div className={s.noResults}>\n              <Typography variant=\"body2\" className={s.noResultsText}>\n                {searchTerm ? 'No options found' : 'All options selected'}\n              </Typography>\n            </div>\n          )}\n        </div>\n      )}\n\n      {/* Error message */}\n      {showError && (\n        <Typography variant=\"error\" className={s.errorMessage}>\n          {errorMessage}\n        </Typography>\n      )}\n\n      {/* Tags counter */}\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} selected\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Autocomplete/index.ts",
    "content": "export * from './Autocomplete'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Button/Button.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: inline-flex;\n  gap: 4px;\n  align-items: center;\n  justify-content: center;\n\n  height: 40px;\n  padding: 8px 16px;\n  border-radius: 45px;\n\n  font-size: var(--font-size-s);\n  font-weight: 600;\n  color: var(--color-text-primary);\n\n  transition: opacity 200ms;\n}\n\n.button:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.button:disabled {\n  cursor: initial;\n  background-color: var(--color-disabled);\n}\n\n.button:hover:not(:disabled),\n.button:focus:not(:disabled) {\n  opacity: 0.8;\n}\n\n.primary {\n  background-color: var(--color-accent);\n}\n\n.secondary {\n  background-color: var(--color-bg-interactive-secondary);\n}\n\n.fullWidth {\n  width: 100%;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Button/Button.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Button } from './Button'\n\nconst meta = {\n  title: 'Components/Button',\n  component: Button,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Button>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllButtons: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '24px',\n        flexDirection: 'column',\n        alignItems: 'center',\n        width: '250px',\n      }}>\n      <Button variant=\"primary\">Primary</Button>\n      <Button variant=\"secondary\">Secondary</Button>\n      <Button fullWidth>Full Width</Button>\n      <Button disabled>Disabled</Button>\n      <Button variant=\"primary\" as=\"p\" href=\"https://it-incubator.io/\" target=\"_blank\">\n        Link\n      </Button>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Button/Button.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './button.module.css'\n\nexport type ButtonVariant = 'primary' | 'secondary'\n\nexport type ButtonProps<T extends ElementType = 'button'> = {\n  as?: T\n  fullWidth?: boolean\n  variant?: ButtonVariant\n} & ComponentProps<T>\n\nexport const Button = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  className,\n  fullWidth = false,\n  variant = 'primary',\n  ...props\n}: ButtonProps<T>) => {\n  const classNames = clsx(s.button, s[variant], fullWidth && s.fullWidth, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Button/index.ts",
    "content": "export * from './Button'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Card/Card.module.css",
    "content": ".card {\n  display: flex;\n  flex-direction: column;\n  padding: 8px;\n  background: var(--color-bg-card);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Card/Card.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from '../Typography'\nimport { Card } from './Card'\n\nconst meta = {\n  title: 'Components/Card',\n  component: Card,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Card>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  render: () => (\n    <Card>\n      <Typography variant=\"h2\">Chill Mix</Typography>\n      <Typography variant=\"body2\" style={{ color: 'var(--color-text-secondary)' }}>\n        Julia Wolf, Khalid, ayokay and more\n      </Typography>\n    </Card>\n  ),\n}\n\nexport const AsSection: Story = {\n  render: () => (\n    <Card as=\"section\">\n      <Typography variant=\"h3\">Card as section</Typography>\n      <Typography variant=\"caption\">You can use any tag via 'as' prop</Typography>\n    </Card>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Card/Card.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType, ReactNode } from 'react'\n\nimport s from './Card.module.css'\n\nexport type CardProps<T extends ElementType = 'div'> = {\n  as?: T\n  className?: string\n  children?: ReactNode\n} & ComponentProps<T>\n\nexport const Card = <T extends ElementType = 'div'>({\n  as: Component = 'div',\n  className,\n  children,\n  ...props\n}: CardProps<T>) => {\n  return (\n    <Component className={clsx(s.card, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Card/index.ts",
    "content": "export * from './Card'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Dialog/Dialog.module.css",
    "content": ".backdrop {\n  position: fixed;\n  z-index: 1;\n  inset: 0;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  background-color: rgb(0 0 0 / 50%);\n\n  animation: fade-in 200ms ease-out;\n}\n\n.dialog {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n\n  max-width: 745px;\n  max-height: 90vh;\n  border-radius: 4px;\n\n  background-color: var(--color-bg-secondary);\n\n  animation: slide-in 200ms ease-out;\n}\n\n.header {\n  display: flex;\n  gap: 16px;\n  align-items: center;\n  justify-content: space-between;\n\n  padding: 18px 24px;\n}\n\n.closeButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: 16px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.closeButton:hover {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  overflow-y: auto;\n  flex: 1;\n  padding: 20px 24px;\n}\n\n.footer {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-bottom: 8px;\n  padding: 18px 24px;\n}\n\n/* Animations */\n@keyframes fade-in {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes slide-in {\n  from {\n    transform: translateY(-500px);\n    opacity: 0;\n  }\n\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n/* Responsive */\n@media (width <= 768px) {\n  .dialog {\n    max-width: 95vw;\n    margin: 20px;\n  }\n\n  .header,\n  .content,\n  .footer {\n    padding-right: 16px;\n    padding-left: 16px;\n  }\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Dialog/Dialog.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from './index'\n\nconst meta = {\n  title: 'Components/Dialog',\n  component: Dialog,\n  parameters: {\n    layout: 'centered',\n  },\n} satisfies Meta<typeof Dialog>\n\nexport default meta\n\nexport const BasicDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Open Basic Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Dialog Title</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <Typography variant=\"body1\">\n              This is dialog content. Here can be any content - text, forms, images and much more.\n            </Typography>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Cancel\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Confirm\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const FormDialog = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Form Dialog</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Sign in to Spotifun</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '16px',\n                minWidth: '320px',\n              }}>\n              <TextField label=\"Email or username\" placeholder=\"Enter email or username\" />\n              <TextField label=\"Password\" type=\"password\" placeholder=\"Enter password\" />\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Continue without signing in\n            </Button>\n            <Button variant=\"primary\" onClick={() => setOpen(false)}>\n              Sign in with API/HUB\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const WithoutCloseButton = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog without close button</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader showCloseButton={false}>\n            <Typography variant=\"h2\">Millions of songs.</Typography>\n            <Typography variant=\"body1\" style={{ color: 'var(--color-text-secondary)' }}>\n              Free on Musicfun.\n            </Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ textAlign: 'center', padding: '20px 0' }}>\n              <div\n                style={{\n                  width: '60px',\n                  height: '60px',\n                  borderRadius: '50%',\n                  backgroundColor: 'var(--color-accent)',\n                  margin: '0 auto 16px',\n                  display: 'flex',\n                  alignItems: 'center',\n                  justifyContent: 'center',\n                  fontSize: '24px',\n                }}>\n                😊\n              </div>\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <div\n              style={{\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '12px',\n                width: '100%',\n              }}>\n              <Button variant=\"primary\" fullWidth onClick={() => setOpen(false)}>\n                Sign up with API/HUB\n              </Button>\n              <Button variant=\"secondary\" fullWidth onClick={() => setOpen(false)}>\n                Continue without signing in\n              </Button>\n            </div>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n\nexport const LongContent = {\n  render: () => {\n    const [open, setOpen] = useState(false)\n\n    return (\n      <>\n        <Button onClick={() => setOpen(true)}>Dialog with long content</Button>\n\n        <Dialog open={open} onClose={() => setOpen(false)}>\n          <DialogHeader>\n            <Typography variant=\"h2\">Long Content</Typography>\n          </DialogHeader>\n\n          <DialogContent>\n            <div style={{ maxWidth: '500px' }}>\n              {Array.from({ length: 20 }, (_, i) => (\n                <Typography key={i} variant=\"body2\" style={{ marginBottom: '12px' }}>\n                  This is paragraph number {i + 1}. Lorem ipsum dolor sit amet, consectetur\n                  adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna\n                  aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\n                </Typography>\n              ))}\n            </div>\n          </DialogContent>\n\n          <DialogFooter>\n            <Button variant=\"secondary\" onClick={() => setOpen(false)}>\n              Close\n            </Button>\n          </DialogFooter>\n        </Dialog>\n      </>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Dialog/Dialog.tsx",
    "content": "import { clsx } from 'clsx'\nimport { createContext, type ReactNode, use, useEffect } from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { IconButton } from '../IconButton'\nimport s from './Dialog.module.css'\n\ntype DialogContextType = {\n  onClose?: () => void\n}\n\nconst DialogContext = createContext<DialogContextType | null>(null)\n\nconst useDialogContext = () => {\n  const context = use(DialogContext)\n  if (!context) {\n    throw new Error('Dialog compound components must be used within Dialog component')\n  }\n  return context\n}\n\nexport type DialogProps = {\n  children: ReactNode\n  open: boolean\n  onClose?: () => void\n  className?: string\n}\n\nexport const Dialog = ({ children, open, onClose, className }: DialogProps) => {\n  const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (e.target === e.currentTarget) {\n      onClose?.()\n    }\n  }\n\n  // Add global keydown handler for ESC key\n  useEffect(() => {\n    if (!open) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose?.()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [open, onClose])\n\n  if (!open) return null\n\n  const dialogContent = (\n    <div className={s.backdrop} onClick={handleBackdropClick} role=\"dialog\" aria-modal=\"true\">\n      <section className={clsx(s.dialog, className)}>\n        <DialogContext value={{ onClose }}>{children}</DialogContext>\n      </section>\n    </div>\n  )\n\n  return createPortal(dialogContent, document.body)\n}\n\n/*\n * DialogHeader\n */\n\nexport type DialogHeaderProps = {\n  children?: ReactNode\n  className?: string\n  showCloseButton?: boolean\n}\n\nexport const DialogHeader = ({\n  children,\n  className,\n  showCloseButton = true,\n}: DialogHeaderProps) => {\n  const { onClose } = useDialogContext()\n\n  return (\n    <header className={clsx(s.header, className)}>\n      <div>{children}</div>\n      {showCloseButton && (\n        <IconButton onClick={onClose} aria-label=\"Close dialog\" type=\"button\">\n          ✕\n        </IconButton>\n      )}\n    </header>\n  )\n}\n\n/*\n * DialogContent\n */\n\nexport type DialogContentProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogContent = ({ children, className }: DialogContentProps) => {\n  return <div className={clsx(s.content, className)}>{children}</div>\n}\n\n/*\n * DialogFooter\n */\n\nexport type DialogFooterProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DialogFooter = ({ children, className }: DialogFooterProps) => {\n  return <footer className={clsx(s.footer, className)}>{children}</footer>\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Dialog/index.ts",
    "content": "export * from './Dialog'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/DropdownMenu/DropdownMenu.module.css",
    "content": ".container {\n  position: relative;\n  display: inline-block;\n}\n\n.trigger {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.trigger:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.trigger:enabled:hover,\n.trigger:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n\n.content {\n  position: fixed;\n  z-index: 50;\n\n  min-width: 160px;\n  padding: 4px;\n  border-radius: 8px;\n\n  background-color: var(--color-bg-primary);\n  box-shadow:\n    0 10px 38px -10px rgb(22 23 24 / 35%),\n    0 10px 20px -15px rgb(22 23 24 / 20%);\n}\n\n.content.align-start {\n  transform-origin: top left;\n}\n\n.content.align-center {\n  transform-origin: top center;\n  transform: translateX(-50%);\n}\n\n.content.align-end {\n  transform-origin: top right;\n  transform: translateX(-100%);\n}\n\n.content.side-top {\n  transform-origin: bottom;\n}\n\n.content.side-top.align-center {\n  transform: translateX(-50%) translateY(-100%);\n}\n\n.content.side-top.align-end {\n  transform: translateX(-100%) translateY(-100%);\n}\n\n.content.side-top.align-start {\n  transform: translateY(-100%);\n}\n\n.item {\n  cursor: pointer;\n\n  display: flex;\n  gap: 8px;\n  align-items: center;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: none;\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-align: left;\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.item:focus-visible {\n  background-color: var(--color-accent);\n  outline: none;\n}\n\n.item:hover:not(:disabled) {\n  background-color: var(--color-accent);\n}\n\n.itemDisabled {\n  cursor: not-allowed;\n  color: var(--color-text-secondary);\n  opacity: 0.5;\n}\n\n.itemDisabled:hover {\n  background: transparent;\n}\n\n.separator {\n  height: 1px;\n  margin: 4px 0;\n  background-color: var(--color-border-base);\n}\n\n/* Animations */\n@keyframes dropdown-menu-show {\n  from {\n    transform: scale(0.95);\n    opacity: 0;\n  }\n\n  to {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { CreateIcon, MoreIcon, PlusIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from './DropdownMenu'\n\nconst meta: Meta<typeof DropdownMenu> = {\n  title: 'Components/DropdownMenu',\n  component: DropdownMenu,\n  parameters: {\n    layout: 'centered',\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const BasicDropdownMenu: Story = {\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked!')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist clicked!')}>\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song clicked!')}>\n          Show text song\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithIcons: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Add to playlist')}>\n          <PlusIcon />\n          Add to playlist\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const WithDisabledItem: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit')}>Edit</DropdownMenuItem>\n        <DropdownMenuItem disabled>Add to playlist (disabled)</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const CustomTrigger: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <IconButton aria-label=\"More options\">\n          <MoreIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Action 1')}>Action 1</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 2')}>Action 2</DropdownMenuItem>\n        <DropdownMenuItem onClick={() => alert('Action 3')}>Action 3</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const DifferentAlignments: Story = {\n  args: {},\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '100px',\n        padding: '100px',\n        alignItems: 'center',\n        backgroundColor: 'var(--color-bg-secondary)',\n      }}>\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align Start\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align Center\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"center\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <div>\n        <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '8px' }}>\n          Align End (default)\n        </Typography>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Add to playlist</DropdownMenuItem>\n            <DropdownMenuItem>Show text song</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </div>\n  ),\n}\n\nexport const WithLinks: Story = {\n  args: {},\n  render: () => (\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        <MoreIcon />\n      </DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem onClick={() => alert('Edit clicked')}>\n          <CreateIcon />\n          Edit\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          as=\"a\"\n          href=\"https://example.com\"\n          target=\"_blank\"\n          onClick={() => console.log('Link clicked')}>\n          <PlusIcon />\n          Visit Website\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onClick={() => alert('Show text song')}>Show text song</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  ),\n}\n\nexport const Interactive: Story = {\n  args: {},\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '20px',\n        alignItems: 'center',\n        padding: '40px',\n      }}>\n      <Typography variant=\"h3\">Click the menu buttons to test functionality</Typography>\n\n      <div style={{ display: 'flex', gap: '20px' }}>\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <MoreIcon />\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent>\n            <DropdownMenuItem onClick={() => console.log('Edit clicked')}>\n              <CreateIcon />\n              Edit track\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Add to playlist clicked')}>\n              <PlusIcon />\n              Add to playlist\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem\n              as=\"a\"\n              href=\"https://example.com\"\n              target=\"_blank\"\n              onClick={() => console.log('External link clicked')}>\n              Show lyrics online\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Download clicked')}>\n              Download\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem disabled>Share (coming soon)</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <IconButton aria-label=\"Playlist options\">\n              <MoreIcon />\n            </IconButton>\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent align=\"start\">\n            <DropdownMenuItem onClick={() => console.log('Edit playlist')}>\n              Edit playlist\n            </DropdownMenuItem>\n            <DropdownMenuItem\n              as=\"a\"\n              href=\"/share/playlist\"\n              onClick={() => console.log('Share playlist')}>\n              Share playlist\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => console.log('Delete playlist')}>\n              Delete playlist\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <Typography variant=\"caption\">Open browser console to see click events</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/DropdownMenu/DropdownMenu.tsx",
    "content": "import { clsx } from 'clsx'\nimport {\n  type ComponentProps,\n  createContext,\n  type ElementType,\n  type ReactNode,\n  use,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\n\nimport s from './DropdownMenu.module.css'\n\ntype DropdownMenuContextType = {\n  isOpen: boolean\n  onClose: () => void\n  onToggle: () => void\n  triggerRef: React.RefObject<HTMLElement | null>\n}\n\nconst DropdownMenuContext = createContext<DropdownMenuContextType | null>(null)\n\nconst useDropdownMenuContext = () => {\n  const context = use(DropdownMenuContext)\n  if (!context) {\n    throw new Error('DropdownMenu compound components must be used within DropdownMenu component')\n  }\n  return context\n}\n\n/*\n * DropdownMenu\n */\n\nexport type DropdownMenuProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const DropdownMenu = ({ children, className }: DropdownMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false)\n  const triggerRef = useRef<HTMLElement>(null)\n\n  const onClose = () => setIsOpen(false)\n  const onToggle = () => setIsOpen(!isOpen)\n\n  useBlockScroll({ isOpen, triggerRef })\n\n  // Close on escape key\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose()\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n    return () => document.removeEventListener('keydown', handleKeyDown)\n  }, [isOpen])\n\n  // Close on click outside\n  useEffect(() => {\n    if (!isOpen) return\n\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as Element\n      if (\n        triggerRef.current &&\n        !triggerRef.current.contains(target) &&\n        !target.closest('[data-dropdown-content]')\n      ) {\n        onClose()\n      }\n    }\n\n    document.addEventListener('mousedown', handleClickOutside)\n    return () => document.removeEventListener('mousedown', handleClickOutside)\n  }, [isOpen])\n\n  const contextValue = {\n    isOpen,\n    onClose,\n    onToggle,\n    triggerRef,\n  }\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <DropdownMenuContext value={contextValue}>{children}</DropdownMenuContext>\n    </div>\n  )\n}\n\n/*\n * DropdownMenuTrigger\n */\n\nexport type DropdownMenuTriggerProps = {\n  children: ReactNode\n  className?: string\n  asChild?: boolean\n}\n\nexport const DropdownMenuTrigger = ({\n  children,\n  className,\n  asChild = false,\n}: DropdownMenuTriggerProps) => {\n  const { onToggle, triggerRef } = useDropdownMenuContext()\n\n  if (asChild) {\n    return (\n      <div\n        ref={triggerRef as React.RefObject<HTMLDivElement>}\n        onClick={onToggle}\n        className={className}>\n        {children}\n      </div>\n    )\n  }\n\n  return (\n    <button\n      ref={triggerRef as React.RefObject<HTMLButtonElement>}\n      type=\"button\"\n      onClick={onToggle}\n      className={clsx(s.trigger, className)}>\n      {children}\n    </button>\n  )\n}\n\n/*\n * DropdownMenuContent\n */\n\nexport type DropdownMenuContentProps = {\n  children: ReactNode\n  className?: string\n  align?: 'start' | 'center' | 'end'\n  side?: 'top' | 'bottom' | 'left' | 'right'\n}\n\nexport const DropdownMenuContent = ({\n  children,\n  className,\n  align = 'end',\n  side = 'bottom',\n}: DropdownMenuContentProps) => {\n  const { isOpen, triggerRef } = useDropdownMenuContext()\n  const [position, setPosition] = useState({ top: 0, left: 0 })\n\n  // it's needed to prevent flickering\n  const [isPositioned, setIsPositioned] = useState(false)\n\n  useEffect(() => {\n    if (!isOpen || !triggerRef.current) {\n      setIsPositioned(false)\n      return\n    }\n\n    const triggerRect = triggerRef.current.getBoundingClientRect()\n    const scrollTop = window.pageYOffset || document.documentElement.scrollTop\n    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft\n\n    let top = 0\n    let left = 0\n\n    // Calculate position based on side\n    switch (side) {\n      case 'bottom':\n        top = triggerRect.bottom + scrollTop + 4\n        break\n      case 'top':\n        top = triggerRect.top + scrollTop - 4\n        break\n      case 'right':\n        left = triggerRect.right + scrollLeft + 4\n        top = triggerRect.top + scrollTop\n        break\n      case 'left':\n        left = triggerRect.left + scrollLeft - 4\n        top = triggerRect.top + scrollTop\n        break\n    }\n\n    // Calculate position based on align\n    if (side === 'bottom' || side === 'top') {\n      switch (align) {\n        case 'start':\n          left = triggerRect.left + scrollLeft\n          break\n        case 'center':\n          left = triggerRect.left + scrollLeft + triggerRect.width / 2\n          break\n        case 'end':\n          left = triggerRect.right + scrollLeft\n          break\n      }\n    }\n\n    setPosition({ top, left })\n    setIsPositioned(true)\n  }, [isOpen, align, side])\n\n  if (!isOpen || !isPositioned) return null\n\n  const content = (\n    <div\n      className={clsx(s.content, s[`align-${align}`], s[`side-${side}`], className)}\n      style={{ top: position.top, left: position.left }}\n      data-dropdown-content\n      role=\"menu\">\n      {children}\n    </div>\n  )\n\n  return createPortal(content, document.body)\n}\n\n/*\n * DropdownMenuItem\n */\n\nexport type DropdownMenuItemProps<T extends ElementType = 'button'> = {\n  as?: T\n  children: ReactNode\n  onClick?: () => void\n  className?: string\n  disabled?: boolean\n} & ComponentProps<T>\n\nexport const DropdownMenuItem = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  children,\n  onClick,\n  className,\n  disabled = false,\n  ...props\n}: DropdownMenuItemProps<T>) => {\n  const { onClose } = useDropdownMenuContext()\n\n  const handleClick = () => {\n    if (disabled) return\n    onClick?.()\n    onClose()\n  }\n\n  const isButton = Component === 'button'\n\n  return (\n    <Component\n      {...(isButton && { type: 'button' })}\n      className={clsx(s.item, disabled && s.itemDisabled, className)}\n      onClick={handleClick}\n      {...(isButton && { disabled })}\n      role=\"menuitem\"\n      {...props}>\n      {children}\n    </Component>\n  )\n}\n\n/*\n * DropdownMenuSeparator\n */\n\nexport type DropdownMenuSeparatorProps = {\n  className?: string\n}\n\nexport const DropdownMenuSeparator = ({ className }: DropdownMenuSeparatorProps) => {\n  return <div className={clsx(s.separator, className)} role=\"separator\" />\n}\n\n/**\n * Block scroll when menu is open.\n */\nconst useBlockScroll = ({\n  isOpen,\n  triggerRef,\n}: {\n  isOpen: boolean\n  triggerRef: React.RefObject<HTMLElement | null>\n}) => {\n  // Block scroll when menu is open\n  useEffect(() => {\n    if (!isOpen || !triggerRef.current) return\n\n    const originalScrollElements: Array<{ element: Element; overflow: string }> = []\n\n    // Find all scrollable parent elements\n    const findScrollableParents = (element: Element) => {\n      const scrollableElements: Element[] = []\n      let parent = element.parentElement\n\n      while (parent && parent !== document.body) {\n        const style = window.getComputedStyle(parent)\n        const hasVerticalScroll =\n          style.overflowY === 'auto' ||\n          style.overflowY === 'scroll' ||\n          style.overflow === 'auto' ||\n          style.overflow === 'scroll'\n\n        if (hasVerticalScroll && parent.scrollHeight > parent.clientHeight) {\n          scrollableElements.push(parent)\n        }\n        parent = parent.parentElement\n      }\n\n      return scrollableElements\n    }\n\n    // Block scroll on body\n    const bodyOverflow = document.body.style.overflow\n    document.body.style.overflow = 'hidden'\n    originalScrollElements.push({ element: document.body, overflow: bodyOverflow })\n\n    // Block scroll on scrollable parents\n    const scrollableParents = findScrollableParents(triggerRef.current)\n    scrollableParents.forEach((element) => {\n      const originalOverflow = (element as HTMLElement).style.overflow\n      ;(element as HTMLElement).style.overflow = 'hidden'\n      originalScrollElements.push({ element, overflow: originalOverflow })\n    })\n\n    return () => {\n      // Restore original overflow values\n      originalScrollElements.forEach(({ element, overflow }) => {\n        ;(element as HTMLElement).style.overflow = overflow\n      })\n    }\n  }, [isOpen])\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/DropdownMenu/index.ts",
    "content": "export * from './DropdownMenu'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Hashtag/Tag.module.css",
    "content": ".hashtag {\n  cursor: pointer;\n\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n\n  min-width: 73px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-base);\n  border-radius: 45px;\n\n  font-size: var(--font-size-xxxs);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  text-decoration: none;\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.hashtag:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.hashtag:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.active {\n  color: var(--color-bg-primary);\n  background-color: var(--color-text-primary);\n}\n\n.active:hover:not(:disabled) {\n  color: var(--color-bg-primary);\n  opacity: 0.9;\n  background-color: var(--color-text-primary);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Hashtag/Tag.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Tag } from './Tag.tsx'\n\nconst meta = {\n  title: 'Components/Hashtag',\n  component: Tag,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    tag: 'Playlists',\n  },\n} satisfies Meta<typeof Tag>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n\nexport const Active: Story = {\n  args: {\n    active: true,\n  },\n}\n\nexport const AsLink: Story = {\n  args: {\n    as: 'a',\n    href: 'https://www.google.com',\n    target: '_blank',\n  },\n}\n\nexport const AllHashtags: Story = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '16px' }}>\n      <Tag tag=\"Playlists\" />\n      <Tag active tag=\"Artists\" />\n      <Tag tag=\"Albums\" />\n      <Tag as=\"a\" href=\"#\" tag=\"Podcasts & shows\">\n        Podcasts & shows\n      </Tag>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Hashtag/Tag.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\n\nimport s from './Tag.module.css'\n\nexport type HashtagProps<T extends ElementType = 'button'> = {\n  as?: T\n  active?: boolean\n  tag: string\n  className?: string\n} & ComponentProps<T>\n\nexport const Tag = <T extends ElementType = 'button'>({\n  as: Component = 'button',\n  active = false,\n  tag,\n  className,\n  ...props\n}: HashtagProps<T>) => {\n  const classNames = clsx(s.hashtag, active && s.active, className)\n\n  return (\n    <Component className={classNames} {...props}>\n      #{tag}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Hashtag/index.ts",
    "content": "export * from './Tag.tsx'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/IconButton/IconButton.module.css",
    "content": ".button {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n\n  font-size: var(--font-size-s);\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.button:disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.button:enabled:hover,\n.button:enabled:focus-visible {\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-input-hover);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/IconButton/IconButton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport {\n  DownloadIcon,\n  HomeIcon,\n  LikeIcon,\n  MoreIcon,\n  PlayIcon,\n  PlusIcon,\n  SearchIcon,\n} from '@/shared/icons'\n\nimport { IconButton } from './IconButton'\n\nconst meta = {\n  title: 'Components/IconButton',\n  component: IconButton,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof IconButton>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    children: <PlayIcon />,\n    'aria-label': 'Play',\n  },\n}\n\nexport const AllIcons = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        gap: '16px',\n        flexWrap: 'wrap',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: '20px',\n      }}>\n      <IconButton aria-label=\"Home\">\n        <HomeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Search\">\n        <SearchIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Play\">\n        <PlayIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Like\">\n        <LikeIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Add\">\n        <PlusIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"More options\">\n        <MoreIcon />\n      </IconButton>\n\n      <IconButton aria-label=\"Download\">\n        <DownloadIcon />\n      </IconButton>\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    children: <PlayIcon />,\n    disabled: true,\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/IconButton/IconButton.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './IconButton.module.css'\n\ntype IconButtonProps = {\n  children: React.ReactNode\n} & ComponentProps<'button'>\n\nexport const IconButton = ({ children, className, ...props }: IconButtonProps) => {\n  return (\n    <button type=\"button\" className={clsx(s.button, className)} {...props}>\n      {children}\n    </button>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/IconButton/index.ts",
    "content": "export * from './IconButton'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/ImageUploader/ImageUploader.module.css",
    "content": ".container {\n  width: 100%;\n}\n\n.dropZone {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 100%;\n  min-height: 280px;\n  border: 2px dashed var(--color-border-input-primary);\n  border-radius: 8px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover,\n.dropZone:focus-within {\n  border-color: var(--color-border-input-active);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.dragOver {\n  border-color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.dropZone.hasPreview {\n  border-color: var(--color-border-input-active);\n  border-style: solid;\n}\n\n.dropZone.error {\n  border-color: var(--color-text-error);\n}\n\n.hiddenInput {\n  position: absolute;\n\n  overflow: hidden;\n\n  width: 1px;\n  height: 1px;\n\n  opacity: 0;\n  clip: rect(0, 0, 0, 0);\n}\n\n.uploadContent {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  align-items: center;\n\n  padding: 32px 16px;\n}\n\n.uploadIcon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n\n  color: var(--color-text-secondary);\n\n  background-color: var(--color-bg-primary);\n\n  transition: all 200ms ease;\n}\n\n.dropZone:hover .uploadIcon,\n.dropZone:focus-within .uploadIcon {\n  color: var(--color-accent);\n  background-color: var(--color-bg-card);\n}\n\n.uploadText {\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  transition: color 200ms ease;\n}\n\n.dropZone:hover .uploadText {\n  color: var(--color-text-primary);\n}\n\n.previewContainer {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.previewImage {\n  width: 100%;\n  height: 100%;\n  min-height: 200px;\n  border-radius: 6px;\n\n  object-fit: cover;\n}\n\n.removeButton {\n  position: absolute;\n  top: 8px;\n  right: 8px;\n}\n\n.removeButton:hover {\n  opacity: 1;\n  background-color: var(--color-text-error);\n}\n\n.removeButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.errorMessage {\n  margin-top: 8px;\n}\n\n/* States for different sizes */\n.dropZone.small {\n  min-height: 120px;\n}\n\n.dropZone.large {\n  min-height: 300px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/ImageUploader/ImageUploader.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ImageUploader } from './ImageUploader'\n\nconst meta = {\n  title: 'Components/ImageUploader',\n  component: ImageUploader,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {\n    onImageSelect: () => {},\n  },\n} satisfies Meta<typeof ImageUploader>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    placeholder: 'Upload Cover Image',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const CustomPlaceholder: Story = {\n  args: {\n    placeholder: 'Choose your avatar',\n  },\n  render: (args) => (\n    <div style={{ width: '200px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const WithCustomLimits: Story = {\n  args: {\n    placeholder: 'Upload image (max 2MB)',\n    maxSizeInMB: 2,\n    acceptedFormats: ['image/jpeg', 'image/png'],\n  },\n  render: (args) => (\n    <div style={{ width: '400px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const AllowAllImages: Story = {\n  args: {\n    placeholder: 'Upload any image format',\n    acceptedFormats: ['image/*'],\n    maxSizeInMB: 10,\n  },\n  render: (args) => (\n    <div style={{ width: '350px' }}>\n      <ImageUploader {...args} />\n    </div>\n  ),\n}\n\nexport const Interactive: Story = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '400px',\n      }}>\n      <div>\n        <h3 style={{ color: 'var(--color-text-primary)', marginBottom: '12px' }}>Profile Avatar</h3>\n        <ImageUploader\n          placeholder=\"Upload avatar\"\n          onImageSelect={(file) => console.log('Avatar selected:', file.name)}\n          maxSizeInMB={1}\n        />\n      </div>\n\n      <div>\n        <h3 style={{ color: 'var(--color-text-primary)', marginBottom: '12px' }}>Playlist Cover</h3>\n        <ImageUploader\n          placeholder=\"Upload Cover Image\"\n          onImageSelect={(file) => console.log('Cover selected:', file.name)}\n          maxSizeInMB={5}\n        />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/ImageUploader/ImageUploader.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ChangeEvent, type DragEvent, useRef, useState } from 'react'\n\nimport { ImageUploadIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { Typography } from '../Typography'\nimport s from './ImageUploader.module.css'\n\nexport type ImageUploaderProps = {\n  onImageSelect: (file: File) => void\n  className?: string\n  acceptedFormats?: string[]\n  maxSizeInMB?: number\n  placeholder?: string\n}\n\nexport const ImageUploader = ({\n  className,\n  onImageSelect,\n  acceptedFormats = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'],\n  maxSizeInMB = 5,\n  placeholder = 'Upload Cover Image',\n}: ImageUploaderProps) => {\n  const [isDragOver, setIsDragOver] = useState(false)\n  const [preview, setPreview] = useState<string | null>(null)\n  const [error, setError] = useState<string | null>(null)\n  const fileInputRef = useRef<HTMLInputElement>(null)\n\n  const validateFile = (file: File): string | null => {\n    if (!acceptedFormats.includes(file.type)) {\n      return `Only ${acceptedFormats.join(', ')} files are allowed`\n    }\n\n    const maxSizeInBytes = maxSizeInMB * 1024 * 1024\n    if (file.size > maxSizeInBytes) {\n      return `File size must be less than ${maxSizeInMB}MB`\n    }\n\n    return null\n  }\n\n  const handleFileSelect = (file: File) => {\n    const validationError = validateFile(file)\n\n    if (validationError) {\n      setError(validationError)\n      setPreview(null)\n      return\n    }\n\n    setError(null)\n\n    // Create preview\n    const reader = new FileReader()\n    reader.onload = (e) => {\n      setPreview(e.target?.result as string)\n    }\n    reader.readAsDataURL(file)\n\n    onImageSelect(file)\n  }\n\n  const handleFileInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0]\n    if (file) {\n      handleFileSelect(file)\n    }\n  }\n\n  const handleDragOver = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(true)\n  }\n\n  const handleDragLeave = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n  }\n\n  const handleDrop = (e: DragEvent) => {\n    e.preventDefault()\n    setIsDragOver(false)\n\n    const files = Array.from(e.dataTransfer.files)\n    const imageFile = files.find((file) => file.type.startsWith('image/'))\n\n    if (imageFile) {\n      handleFileSelect(imageFile)\n    }\n  }\n\n  const handleRemoveImage = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    setPreview(null)\n    setError(null)\n    // Clear input value to allow selecting the same file again\n    if (fileInputRef.current) {\n      fileInputRef.current.value = ''\n    }\n  }\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <label\n        className={clsx(\n          s.dropZone,\n          isDragOver && s.dragOver,\n          preview && s.hasPreview,\n          error && s.error\n        )}\n        onDragOver={handleDragOver}\n        onDragLeave={handleDragLeave}\n        onDrop={handleDrop}>\n        <input\n          ref={fileInputRef}\n          type=\"file\"\n          accept={acceptedFormats.join(',')}\n          onChange={handleFileInputChange}\n          className={s.hiddenInput}\n          tabIndex={0}\n        />\n\n        {preview ? (\n          <div className={s.previewContainer}>\n            <img src={preview} alt=\"Preview\" className={s.previewImage} />\n            <IconButton\n              className={s.removeButton}\n              onClick={handleRemoveImage}\n              aria-label=\"Remove image\"\n              type=\"button\">\n              ✕\n            </IconButton>\n          </div>\n        ) : (\n          <div className={s.uploadContent}>\n            <div className={s.uploadIcon}>\n              <ImageUploadIcon width={24} height={24} />\n            </div>\n            <Typography variant=\"body2\" className={s.uploadText}>\n              {placeholder}\n            </Typography>\n          </div>\n        )}\n      </label>\n\n      {error && (\n        <Typography variant=\"error\" className={s.errorMessage}>\n          {error}\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/ImageUploader/index.ts",
    "content": "export * from './ImageUploader'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Pagination/Pagination.module.css",
    "content": ".pagination {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n}\n\n.navButton {\n  width: 40px;\n  height: 40px;\n  border-radius: 4px;\n\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.navButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n  background-color: var(--color-bg-secondary);\n}\n\n.navButton:enabled:hover,\n.navButton:enabled:focus {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageNumbers {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n}\n\n.pageButton {\n  cursor: pointer;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n  border: none;\n  border-radius: 8px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.pageButton:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.pageButton:hover:not(.active) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.pageButton.active {\n  background-color: var(--color-accent);\n}\n\n.pageButton.active:hover {\n  opacity: 0.9;\n}\n\n.ellipsis {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: 40px;\n  height: 40px;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* Responsive adjustments */\n@media (width <= 480px) {\n  .pagination {\n    gap: 2px;\n  }\n\n  .navButton,\n  .pageButton,\n  .ellipsis {\n    width: 36px;\n    height: 36px;\n  }\n\n  .pageButton,\n  .ellipsis {\n    font-size: var(--font-size-s);\n  }\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Pagination/Pagination.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Pagination } from './Pagination'\n\nconst meta = {\n  title: 'Components/Pagination',\n  component: Pagination,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Pagination>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    page: 1,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const MiddlePage: Story = {\n  args: {\n    page: 5,\n    pagesCount: 10,\n    onPageChange: () => {},\n  },\n}\n\nexport const LastPage: Story = {\n  args: {\n    page: 3,\n    pagesCount: 3,\n    onPageChange: () => {},\n  },\n}\n\nexport const ManyPages: Story = {\n  args: {\n    page: 8,\n    pagesCount: 20,\n    onPageChange: () => {},\n  },\n}\n\nexport const SinglePage: Story = {\n  args: {\n    page: 1,\n    pagesCount: 1,\n    onPageChange: () => {},\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [currentPage, setCurrentPage] = useState(1)\n    const totalCount = 95\n    const pageSize = 10\n    const pagesCount = Math.ceil(totalCount / pageSize)\n\n    const handlePageChange = (page: number) => {\n      setCurrentPage(page)\n    }\n\n    return (\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n          alignItems: 'center',\n          width: '500px',\n        }}>\n        <Card style={{ padding: '20px', textAlign: 'center' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n            Interactive Pagination\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Current page: <strong>{currentPage}</strong>\n          </Typography>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Total items: <strong>{totalCount}</strong>\n          </Typography>\n          <Typography variant=\"body2\">\n            Items per page: <strong>{pageSize}</strong>\n          </Typography>\n        </Card>\n\n        <Pagination page={currentPage} pagesCount={pagesCount} onPageChange={handlePageChange} />\n\n        <Typography variant=\"caption\" style={{ textAlign: 'center' }}>\n          Click on page numbers or arrows to navigate\n        </Typography>\n      </div>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '32px',\n        alignItems: 'center',\n        width: '600px',\n      }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          First Page (3 pages total)\n        </Typography>\n        <Pagination page={1} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Middle Page (10 pages total)\n        </Typography>\n        <Pagination page={5} pagesCount={10} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Last Page (3 pages total)\n        </Typography>\n        <Pagination page={3} pagesCount={3} onPageChange={() => {}} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '12px', textAlign: 'center' }}>\n          Many Pages (20 pages total)\n        </Typography>\n        <Pagination page={12} pagesCount={20} onPageChange={() => {}} />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Pagination/Pagination.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport { KeyboardArrowLeftIcon, KeyboardArrowRightIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './Pagination.module.css'\n\nexport type PaginationProps = {\n  page: number\n  pagesCount: number\n  onPageChange: (page: number) => void\n  className?: string\n} & Omit<ComponentProps<'div'>, 'children'>\n\nconst MAX_VISIBLE_PAGES = 5\n\nexport const Pagination = ({\n  page,\n\n  pagesCount,\n  onPageChange,\n  className,\n  ...props\n}: PaginationProps) => {\n  // Helper function to generate page numbers array\n  const generatePageNumbers = () => {\n    const pages: (number | 'ellipsis')[] = []\n\n    if (pagesCount <= MAX_VISIBLE_PAGES) {\n      // Show all pages if total is small\n      for (let i = 1; i <= pagesCount; i++) {\n        pages.push(i)\n      }\n    } else {\n      // Always show first page\n      pages.push(1)\n\n      if (page > 3) {\n        pages.push('ellipsis')\n      }\n\n      // Show pages around current page\n      const start = Math.max(2, page - 1)\n      const end = Math.min(pagesCount - 1, page + 1)\n\n      for (let i = start; i <= end; i++) {\n        if (i !== 1 && i !== pagesCount) {\n          pages.push(i)\n        }\n      }\n\n      if (page < pagesCount - 2) {\n        pages.push('ellipsis')\n      }\n\n      // Always show last page if it's not already included\n      if (pagesCount > 1) {\n        pages.push(pagesCount)\n      }\n    }\n\n    return pages\n  }\n\n  const handlePrevious = () => {\n    if (page > 1) {\n      onPageChange(page - 1)\n    }\n  }\n\n  const handleNext = () => {\n    if (page < pagesCount) {\n      onPageChange(page + 1)\n    }\n  }\n\n  const handlePageClick = (pageNumber: number) => {\n    onPageChange(pageNumber)\n  }\n\n  if (pagesCount <= 1) {\n    return null\n  }\n\n  const pageNumbers = generatePageNumbers()\n\n  return (\n    <div\n      className={clsx(s.pagination, className)}\n      role=\"navigation\"\n      aria-label=\"Pagination\"\n      {...props}>\n      {/* Previous button */}\n      <IconButton\n        onClick={handlePrevious}\n        disabled={page === 1}\n        aria-label=\"Go to previous page\"\n        className={s.navButton}>\n        <KeyboardArrowLeftIcon />\n      </IconButton>\n\n      {/* Page numbers */}\n      <div className={s.pageNumbers}>\n        {pageNumbers.map((pageNumber, index) => {\n          if (pageNumber === 'ellipsis') {\n            return (\n              <span key={`ellipsis-${index}`} className={s.ellipsis} aria-hidden=\"true\">\n                ...\n              </span>\n            )\n          }\n\n          const isActive = pageNumber === page\n\n          return (\n            <button\n              key={pageNumber}\n              onClick={() => handlePageClick(pageNumber)}\n              className={clsx(s.pageButton, isActive && s.active)}\n              aria-label={`Go to page ${pageNumber}`}\n              aria-current={isActive ? 'page' : undefined}\n              type=\"button\">\n              {pageNumber}\n            </button>\n          )\n        })}\n      </div>\n\n      {/* Next button */}\n      <IconButton\n        onClick={handleNext}\n        disabled={page === pagesCount}\n        aria-label=\"Go to next page\"\n        className={s.navButton}>\n        <KeyboardArrowRightIcon />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Pagination/index.ts",
    "content": "export * from './Pagination'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Progress/Progress.module.css",
    "content": ".progress {\n  overflow: hidden;\n\n  width: 100%;\n  height: 4px;\n  border-radius: 4px;\n\n  background-color: var(--color-border-base);\n}\n\n.progressBar {\n  height: 100%;\n  border-radius: 4px;\n  background-color: var(--color-accent);\n  transition: width 300ms ease;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Progress/Progress.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Progress } from './Progress'\n\nconst meta = {\n  title: 'Components/Progress',\n  component: Progress,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Progress>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    value: 75,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const CustomMax: Story = {\n  args: {\n    value: 15,\n    max: 20,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Empty: Story = {\n  args: {\n    value: 0,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const Full: Story = {\n  args: {\n    value: 100,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Progress {...args} />\n    </div>\n  ),\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Empty (0%)\n        </Typography>\n        <Progress value={0} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Low (25%)\n        </Typography>\n        <Progress value={25} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Medium (50%)\n        </Typography>\n        <Progress value={50} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          High (85%)\n        </Typography>\n        <Progress value={85} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Complete (100%)\n        </Typography>\n        <Progress value={100} />\n      </div>\n    </div>\n  ),\n}\n\nexport const CustomSizes = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '400px' }}>\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Small (height: 4px)\n        </Typography>\n        <Progress value={70} style={{ height: '4px' }} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Default (height: 8px)\n        </Typography>\n        <Progress value={70} />\n      </div>\n\n      <div>\n        <Typography variant=\"h3\" style={{ marginBottom: '8px' }}>\n          Large (height: 12px)\n        </Typography>\n        <Progress value={70} style={{ height: '12px' }} />\n      </div>\n    </div>\n  ),\n}\n\nexport const Interactive = {\n  render: () => {\n    const [progress, setProgress] = useState(0)\n\n    const handleIncrease = () => {\n      setProgress((prev) => Math.min(prev + 10, 100))\n    }\n\n    const handleDecrease = () => {\n      setProgress((prev) => Math.max(prev - 10, 0))\n    }\n\n    const handleReset = () => {\n      setProgress(0)\n    }\n\n    return (\n      <div style={{ width: '400px' }}>\n        <Card style={{ padding: '24px' }}>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Interactive Progress\n          </Typography>\n\n          <div style={{ marginBottom: '16px' }}>\n            <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n              Current progress: {progress}%\n            </Typography>\n            <Progress value={progress} />\n          </div>\n\n          <div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>\n            <Button variant=\"secondary\" onClick={handleDecrease} disabled={progress === 0}>\n              -10%\n            </Button>\n            <Button variant=\"secondary\" onClick={handleReset}>\n              Reset\n            </Button>\n            <Button variant=\"primary\" onClick={handleIncrease} disabled={progress === 100}>\n              +10%\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const FileUploadExample = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Card style={{ padding: '24px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          File Upload Progress\n        </Typography>\n\n        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">image.jpg</Typography>\n              <Typography variant=\"body2\">75%</Typography>\n            </div>\n            <Progress value={75} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">document.pdf</Typography>\n              <Typography variant=\"body2\">100%</Typography>\n            </div>\n            <Progress value={100} />\n          </div>\n\n          <div>\n            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>\n              <Typography variant=\"body2\">video.mp4</Typography>\n              <Typography variant=\"body2\">32%</Typography>\n            </div>\n            <Progress value={32} />\n          </div>\n        </div>\n      </Card>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Progress/Progress.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps } from 'react'\n\nimport s from './Progress.module.css'\n\nexport type ProgressProps = {\n  value: number\n  max?: number\n} & ComponentProps<'div'>\n\nexport const Progress = ({ value, max = 100, className, ...props }: ProgressProps) => {\n  const percentage = Math.min(Math.max((value / max) * 100, 0), 100)\n\n  return (\n    <div\n      className={clsx(s.progress, className)}\n      role=\"progressbar\"\n      aria-valuenow={value}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      {...props}>\n      <div className={s.progressBar} style={{ width: `${percentage}%` }} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Progress/index.ts",
    "content": "export * from './Progress'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/ReactionButtons/ReactionButtons.module.css",
    "content": ".container {\n  display: flex;\n  gap: 8px;\n  align-items: start;\n}\n\n.button {\n  width: 28px;\n  height: 28px;\n  padding: 0;\n  transition: color 200ms ease;\n}\n\n.button.large {\n  width: 40px;\n  height: 40px;\n}\n\n.button.liked {\n  color: var(--color-accent);\n}\n\n.button.disliked {\n  color: var(--color-accent);\n}\n\n.button:enabled:hover:is(.liked, .disliked),\n.button:enabled:focus:is(.liked, .disliked) {\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.likesCountBox {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.likesCount {\n  font-size: 10px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/ReactionButtons/ReactionButtons.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { CurrentUserReaction, ReactionButtons } from './ReactionButtons'\n\nconst meta = {\n  title: 'Components/ReactionButtons',\n  component: ReactionButtons,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof ReactionButtons>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    reaction: CurrentUserReaction.None,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n  },\n}\n\nexport const WithLikesCount: Story = {\n  args: {\n    reaction: CurrentUserReaction.None,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Disliked!'),\n    likesCount: 10,\n  },\n}\n\nexport const LikedState: Story = {\n  args: {\n    reaction: CurrentUserReaction.Like,\n    onLike: () => console.log('Unlike'),\n    onDislike: () => console.log('Disliked!'),\n  },\n}\n\nexport const DislikedState: Story = {\n  args: {\n    reaction: CurrentUserReaction.Dislike,\n    onLike: () => console.log('Liked!'),\n    onDislike: () => console.log('Remove dislike'),\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [reaction, setReaction] = useState<CurrentUserReaction>(CurrentUserReaction.None)\n\n    const handleLike = () => {\n      setReaction(\n        reaction === CurrentUserReaction.Like ? CurrentUserReaction.None : CurrentUserReaction.Like\n      )\n    }\n\n    const handleDislike = () => {\n      setReaction(\n        reaction === CurrentUserReaction.Dislike\n          ? CurrentUserReaction.None\n          : CurrentUserReaction.Dislike\n      )\n    }\n\n    return (\n      <Card style={{ padding: '24px', maxWidth: '300px' }}>\n        <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n          Interactive Reaction Buttons\n        </Typography>\n\n        <Typography variant=\"body2\" style={{ marginBottom: '16px' }}>\n          Try clicking the buttons below:\n        </Typography>\n\n        <div style={{ display: 'flex', justifyContent: 'center' }}>\n          <ReactionButtons reaction={reaction} onLike={handleLike} onDislike={handleDislike} />\n        </div>\n\n        <Typography\n          variant=\"caption\"\n          style={{ marginTop: '16px', textAlign: 'center', display: 'block' }}>\n          Status:{' '}\n          {reaction === CurrentUserReaction.Like\n            ? '👍 Liked'\n            : reaction === CurrentUserReaction.Dislike\n              ? '👎 Disliked'\n              : '😐 Neutral'}\n        </Typography>\n      </Card>\n    )\n  },\n}\n\nexport const AllStates = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Default\n        </Typography>\n        <ReactionButtons\n          reaction={CurrentUserReaction.None}\n          onLike={() => {}}\n          onDislike={() => {}}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Liked\n        </Typography>\n        <ReactionButtons\n          reaction={CurrentUserReaction.Like}\n          onLike={() => {}}\n          onDislike={() => {}}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          Disliked\n        </Typography>\n        <ReactionButtons\n          reaction={CurrentUserReaction.Dislike}\n          onLike={() => {}}\n          onDislike={() => {}}\n        />\n      </div>\n\n      <div style={{ textAlign: 'center' }}>\n        <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n          With likes count\n        </Typography>\n        <ReactionButtons\n          reaction={CurrentUserReaction.None}\n          onLike={() => {}}\n          onDislike={() => {}}\n          likesCount={10}\n        />\n      </div>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/ReactionButtons/ReactionButtons.tsx",
    "content": "import { clsx } from 'clsx'\n\nimport { DislikeIcon, LikeIcon, LikeIconFill } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport s from './ReactionButtons.module.css'\n\n// duplication of the CurrentUserReaction type to decouple the shared layer from the features layer\nexport enum CurrentUserReaction {\n  None = 0,\n  Like = 1,\n  Dislike = 2,\n}\n\nexport type ReactionButtonsProps = {\n  reaction: CurrentUserReaction\n  onLike: () => void\n  onDislike: () => void\n  likesCount?: number\n  className?: string\n  size?: keyof typeof SIZE_MAP\n}\n\nconst SIZE_MAP = {\n  small: 28,\n  large: 40,\n}\n\nexport const ReactionButtons = ({\n  reaction = CurrentUserReaction.None,\n  onLike,\n  onDislike,\n  likesCount,\n  className,\n  size = 'small',\n}: ReactionButtonsProps) => {\n  const isLiked = reaction === CurrentUserReaction.Like\n  const isDisliked = reaction === CurrentUserReaction.Dislike\n\n  const iconSize = SIZE_MAP[size]\n\n  return (\n    <div className={clsx(s.container, className)}>\n      <div className={s.likesCountBox}>\n        <IconButton\n          onClick={(e) => {\n            e.preventDefault()\n            onLike()\n          }}\n          className={clsx(s.button, isLiked && s.liked, size === 'large' && s.large)}\n          aria-label={isLiked ? 'Remove like' : 'Like'}\n          type=\"button\">\n          {isLiked ? (\n            <LikeIconFill width={iconSize} height={iconSize} />\n          ) : (\n            <LikeIcon width={iconSize} height={iconSize} />\n          )}\n        </IconButton>\n        <span className={s.likesCount}>{likesCount}</span>\n      </div>\n\n      <IconButton\n        onClick={(e) => {\n          e.preventDefault()\n          onDislike()\n        }}\n        className={clsx(s.button, isDisliked && s.disliked, size === 'large' && s.large)}\n        aria-label={isDisliked ? 'Remove dislike' : 'Dislike'}\n        type=\"button\">\n        <DislikeIcon width={iconSize} height={iconSize} />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/ReactionButtons/index.ts",
    "content": "export * from './ReactionButtons'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/SearchField/SearchField.module.css",
    "content": ".inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.searchIcon {\n  pointer-events: none;\n\n  position: absolute;\n  z-index: 1;\n  left: 12px;\n\n  color: var(--color-text-secondary);\n\n  transition: color 200ms ease;\n}\n\n.input {\n  width: 100%;\n  height: 52px;\n  padding: 15px 16px 15px 62px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 26px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary-reverse);\n\n  background-color: var(--color-bg-primary-reverse);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input::placeholder {\n  font-size: var(--font-size-m);\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/SearchField/SearchField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchField } from './SearchField'\n\nconst meta = {\n  title: 'Components/SearchField',\n  component: SearchField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof SearchField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Basic: Story = {\n  args: {\n    placeholder: 'Search for playlists...',\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/SearchField/SearchField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport s from './SearchField.module.css'\n\nexport type SearchFieldProps = {\n  label?: ReactNode\n  placeholder?: string\n} & ComponentProps<'input'>\n\nexport const SearchField = ({\n  className,\n  placeholder = 'Search...',\n  ...props\n}: SearchFieldProps) => {\n  return (\n    <div className={clsx(s.inputWrapper, className)}>\n      <SearchIcon className={s.searchIcon} />\n      <input className={clsx(s.input)} type=\"text\" placeholder={placeholder} {...props} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/SearchField/index.ts",
    "content": "export * from './SearchField'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Select/Select.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.selectWrapper {\n  position: relative;\n  width: 100%;\n}\n\n.select {\n  width: 100%;\n  height: 40px;\n  padding: 8px 36px 8px 12px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  text-decoration: underline;\n  text-underline-offset: 3px;\n\n  appearance: none;\n  background-color: transparent;\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color;\n}\n\n.select:disabled {\n  cursor: not-allowed;\n  color: var(--color-disabled);\n}\n\n.select:focus-visible {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select.error {\n  border-color: var(--color-text-error);\n}\n\n/* Style dropdown options */\n.select option {\n  padding: 8px 12px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-secondary);\n\n  transition: background-color 200ms ease;\n}\n\n.select option:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:checked {\n  font-weight: 600;\n  color: var(--color-accent);\n  background-color: var(--color-bg-input-hover);\n}\n\n.select option:disabled {\n  color: var(--color-disabled);\n}\n\n/* Custom dropdown icon */\n.icon {\n  pointer-events: none;\n\n  position: absolute;\n  top: 50%;\n  right: 12px;\n  transform: translateY(-50%);\n\n  width: 20px;\n  height: 20px;\n\n  color: var(--color-text-secondary);\n\n  transition:\n    color 200ms ease,\n    transform 200ms ease;\n}\n\n/* Rotate icon when dropdown is open */\n.select:open + .icon {\n  transform: translateY(-50%) rotate(180deg);\n}\n\n.label.error {\n  color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Select/Select.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Select } from './Select'\n\nconst meta = {\n  title: 'Components/Select',\n  component: Select,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Select>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst commonOptions = [\n  { value: 'react', label: 'React' },\n  { value: 'vue', label: 'Vue.js' },\n  { value: 'angular', label: 'Angular' },\n  { value: 'svelte', label: 'Svelte' },\n  { value: 'vanilla', label: 'Vanilla JS' },\n]\n\nconst genres = [\n  { value: 'pop', label: 'Pop' },\n  { value: 'rock', label: 'Rock' },\n  { value: 'jazz', label: 'Jazz' },\n  { value: 'classical', label: 'Classical' },\n  { value: 'electronic', label: 'Electronic' },\n  { value: 'hip-hop', label: 'Hip Hop' },\n  { value: 'country', label: 'Country' },\n]\n\nexport const AllVariants = {\n  render: () => (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        gap: '24px',\n        width: '350px',\n      }}>\n      <Select label=\"Basic Select\" placeholder=\"Choose option\" options={commonOptions} />\n\n      <Select label=\"With Default Value\" options={commonOptions} defaultValue=\"react\" />\n\n      <Select\n        label=\"With Error\"\n        placeholder=\"Choose option\"\n        options={commonOptions}\n        errorMessage=\"This field is required\"\n      />\n\n      <Select label=\"Disabled\" placeholder=\"Cannot select\" options={commonOptions} disabled />\n    </div>\n  ),\n}\n\nexport const Basic: Story = {\n  args: {\n    label: 'Choose framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDefaultValue: Story = {\n  args: {\n    label: 'Preferred framework',\n    options: commonOptions,\n    defaultValue: 'react',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Framework (disabled)',\n    placeholder: 'Cannot select',\n    options: commonOptions,\n    disabled: true,\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithError: Story = {\n  args: {\n    label: 'Framework',\n    placeholder: 'Select a framework',\n    options: commonOptions,\n    errorMessage: 'Please select a framework',\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const WithDisabledOptions: Story = {\n  args: {\n    label: 'Music Genre',\n    placeholder: 'Choose your favorite genre',\n    options: [\n      { value: 'pop', label: 'Pop' },\n      { value: 'rock', label: 'Rock' },\n      { value: 'jazz', label: 'Jazz (Coming Soon)', disabled: true },\n      { value: 'classical', label: 'Classical' },\n      { value: 'electronic', label: 'Electronic (Coming Soon)', disabled: true },\n      { value: 'hip-hop', label: 'Hip Hop' },\n    ],\n  },\n  render: (args) => (\n    <div style={{ width: '300px' }}>\n      <Select {...args} />\n    </div>\n  ),\n}\n\nexport const Controlled = {\n  render: () => {\n    const [value, setValue] = useState('')\n\n    return (\n      <div style={{ width: '400px', display: 'flex', flexDirection: 'column', gap: '16px' }}>\n        <Select\n          label=\"Music Genre\"\n          placeholder=\"Select genre\"\n          options={genres}\n          value={value}\n          onChange={(e) => setValue(e.target.value)}\n        />\n\n        <div\n          style={{\n            padding: '12px',\n            backgroundColor: 'var(--color-bg-card)',\n            borderRadius: '4px',\n            fontSize: 'var(--font-size-s)',\n            color: 'var(--color-text-secondary)',\n          }}>\n          Selected value: <strong>{value || 'None'}</strong>\n        </div>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Select/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography\n          variant=\"label\"\n          as=\"label\"\n          htmlFor={selectId}\n          className={clsx(s.label, showError && s.error)}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Select/index.ts",
    "content": "export * from './Select'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/SortSelect/Select.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { ArrowDownIcon } from '@/shared/icons'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Select.module.css'\n\nexport type SelectOption = {\n  value: string\n  label: string\n  disabled?: boolean\n}\n\nexport type SelectProps = {\n  label?: ReactNode\n  errorMessage?: string\n  options: SelectOption[]\n  placeholder?: string\n} & ComponentProps<'select'>\n\nexport const Select = ({\n  className,\n  errorMessage,\n  id,\n  label,\n  options,\n  placeholder,\n  ...props\n}: SelectProps) => {\n  const showError = Boolean(errorMessage)\n  const selectId = useGetId(id)\n\n  return (\n    <div className={clsx(s.container, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={selectId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.selectWrapper}>\n        <select className={clsx(s.select, showError && s.error)} id={selectId} {...props}>\n          {placeholder && (\n            <option value=\"\" disabled>\n              {placeholder}\n            </option>\n          )}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n            </option>\n          ))}\n        </select>\n        <ArrowDownIcon className={s.icon} />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Table/Table.module.css",
    "content": ".table {\n  table-layout: fixed;\n  border-collapse: collapse;\n  width: 100%;\n  background: transparent;\n}\n\n.tableHead {\n  border-bottom: 1px solid var(--color-border-base);\n}\n\n.tableHeaderCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-xs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  text-align: left;\n  text-transform: uppercase;\n\n  background: transparent;\n}\n\n.tableHeaderCell:first-child {\n  padding-left: 16px;\n}\n\n.tableHeaderCell:last-child {\n  padding-right: 16px;\n}\n\n.tableBody {\n  background: transparent;\n}\n\n.tableRow {\n  transition: background-color 200ms ease;\n}\n\n.tableBody .tableRow:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tableCell {\n  padding: 10px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n  vertical-align: middle;\n\n  background: transparent;\n}\n\n.tableCell:first-child {\n  padding-left: 16px;\n}\n\n.tableCell:last-child {\n  padding-right: 16px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Table/Table.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { ReactionButtons } from '../ReactionButtons'\nimport { Typography } from '../Typography'\nimport { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './Table'\nimport s from './Table.module.css'\n\nconst meta = {\n  title: 'Components/Table',\n  component: Table,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Table>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nconst trackData = [\n  {\n    id: 1,\n    title: 'Play It Safe',\n    artist: 'Julia Wolf',\n    image: 'https://picsum.photos/40/40?random=1',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 2,\n    title: 'Ocean Front Apt.',\n    artist: 'ayokay',\n    image: 'https://picsum.photos/40/40?random=2',\n    dateAdded: '1 day ago',\n    duration: '2:12',\n  },\n  {\n    id: 3,\n    title: 'Free Spirit',\n    artist: 'Khalid',\n    image: 'https://picsum.photos/40/40?random=3',\n    dateAdded: '2 day ago',\n    duration: '3:02',\n  },\n  {\n    id: 4,\n    title: 'Remind You',\n    artist: 'FRENSHIP',\n    image: 'https://picsum.photos/40/40?random=4',\n    dateAdded: '3 day ago',\n    duration: '4:25',\n  },\n]\n\nexport const BasicTable = {\n  render: () => (\n    <div style={{ width: '600px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Name</TableHeaderCell>\n            <TableHeaderCell>Email</TableHeaderCell>\n            <TableHeaderCell>Role</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell>John Doe</TableCell>\n            <TableCell>john@example.com</TableCell>\n            <TableCell>Admin</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Jane Smith</TableCell>\n            <TableCell>jane@example.com</TableCell>\n            <TableCell>User</TableCell>\n          </TableRow>\n          <TableRow>\n            <TableCell>Bob Johnson</TableCell>\n            <TableCell>bob@example.com</TableCell>\n            <TableCell>Editor</TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n\nexport const EmptyTable = {\n  render: () => (\n    <div style={{ width: '500px' }}>\n      <Table>\n        <TableHead>\n          <TableRow>\n            <TableHeaderCell>Column&nbsp;1</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;2</TableHeaderCell>\n            <TableHeaderCell>Column&nbsp;3</TableHeaderCell>\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          <TableRow>\n            <TableCell colSpan={3}>\n              <Typography variant=\"body2\" style={{ textAlign: 'center', padding: '40px 20px' }}>\n                No data available\n              </Typography>\n            </TableCell>\n          </TableRow>\n        </TableBody>\n      </Table>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Table/Table.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport s from './Table.module.css'\n\n/*\n * Table\n */\n\nexport type TableProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'table'>\n\nexport const Table = ({ children, className, ...props }: TableProps) => {\n  return (\n    <table className={clsx(s.table, className)} {...props}>\n      {children}\n    </table>\n  )\n}\n\n/*\n * TableHead\n */\n\nexport type TableHeadProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'thead'>\n\nexport const TableHead = ({ children, className, ...props }: TableHeadProps) => {\n  return (\n    <thead className={clsx(s.tableHead, className)} {...props}>\n      {children}\n    </thead>\n  )\n}\n\n/*\n * TableBody\n */\n\nexport type TableBodyProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tbody'>\n\nexport const TableBody = ({ children, className, ...props }: TableBodyProps) => {\n  return (\n    <tbody className={clsx(s.tableBody, className)} {...props}>\n      {children}\n    </tbody>\n  )\n}\n\n/*\n * TableRow\n */\n\nexport type TableRowProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'tr'>\n\nexport const TableRow = ({ children, className, ...props }: TableRowProps) => {\n  return (\n    <tr className={clsx(s.tableRow, className)} {...props}>\n      {children}\n    </tr>\n  )\n}\n\n/*\n * TableHeaderCell\n */\n\nexport type TableHeaderCellProps = {\n  children?: ReactNode\n  className?: string\n} & ComponentProps<'th'>\n\nexport const TableHeaderCell = ({ children, className, ...props }: TableHeaderCellProps) => {\n  return (\n    <th className={clsx(s.tableHeaderCell, className)} {...props}>\n      {children}\n    </th>\n  )\n}\n\n/*\n * TableCell\n */\n\nexport type TableCellProps = {\n  children: ReactNode\n  className?: string\n} & ComponentProps<'td'>\n\nexport const TableCell = ({ children, className, ...props }: TableCellProps) => {\n  return (\n    <td className={clsx(s.tableCell, className)} {...props}>\n      {children}\n    </td>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Table/index.ts",
    "content": "export * from './Table'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Tabs/Tabs.module.css",
    "content": ".tabsList {\n  display: flex;\n  width: 100%;\n  border-bottom: 1px solid var(--color-text-secondary);\n}\n\n.tabsTrigger {\n  cursor: pointer;\n\n  position: relative;\n\n  display: flex;\n  flex: 1 1 0;\n  align-items: center;\n  justify-content: center;\n\n  padding: 12px 16px;\n  border: none;\n\n  font-size: var(--font-size-m);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.tabsTrigger:focus-visible {\n  outline: 2px solid var(--color-outline-focus);\n  outline-offset: 2px;\n}\n\n.tabsTrigger:not(.active, :disabled):hover {\n  opacity: 0.7;\n}\n\n.tabsTrigger.active {\n  color: var(--color-accent);\n}\n\n.tabsTrigger.active::after {\n  content: '';\n\n  position: absolute;\n  bottom: -1px;\n  left: 0;\n\n  width: 100%;\n  height: 2px;\n\n  background-color: var(--color-accent);\n}\n\n.tabsTrigger.disabled {\n  cursor: default;\n  color: var(--color-disabled);\n}\n\n.tabsContent {\n  padding: 32px 0;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Tabs/Tabs.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Button } from '../Button'\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs'\n\nconst meta = {\n  title: 'Components/Tabs',\n  component: Tabs,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Tabs>\n\nexport default meta\n\nexport const BasicTabs = {\n  render: () => (\n    <div style={{ width: '400px' }}>\n      <Tabs defaultValue=\"account\">\n        <TabsList>\n          <TabsTrigger value=\"account\">Account</TabsTrigger>\n          <TabsTrigger value=\"password\">Password</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"account\">\n          <Typography variant=\"body1\">Make changes to your account here.</Typography>\n        </TabsContent>\n        <TabsContent value=\"password\">\n          <Typography variant=\"body1\">Change your password here.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n\nexport const ControlledTabs = {\n  render: () => {\n    const [activeTab, setActiveTab] = useState('tab1')\n\n    return (\n      <div style={{ width: '500px' }}>\n        <Tabs value={activeTab} onValueChange={setActiveTab}>\n          <TabsList>\n            <TabsTrigger value=\"tab1\">Tab 1</TabsTrigger>\n            <TabsTrigger value=\"tab2\">Tab 2</TabsTrigger>\n            <TabsTrigger value=\"tab3\">Tab 3</TabsTrigger>\n          </TabsList>\n          <TabsContent value=\"tab1\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              First Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the first tab. You can put any React content here.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab2\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Second Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              This is content for the second tab with different information.\n            </Typography>\n          </TabsContent>\n          <TabsContent value=\"tab3\">\n            <Typography variant=\"h3\" style={{ marginBottom: '12px' }}>\n              Third Tab Content\n            </Typography>\n            <Typography variant=\"body2\">\n              And this is the third tab with its own unique content.\n            </Typography>\n          </TabsContent>\n        </Tabs>\n\n        <Card\n          style={{\n            marginTop: '20px',\n          }}>\n          <Typography variant=\"body2\">\n            Active tab: <strong>{activeTab}</strong>\n          </Typography>\n          <div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab1')}>\n              Go to Tab 1\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab2')}>\n              Go to Tab 2\n            </Button>\n            <Button variant=\"secondary\" onClick={() => setActiveTab('tab3')}>\n              Go to Tab 3\n            </Button>\n          </div>\n        </Card>\n      </div>\n    )\n  },\n}\n\nexport const DisabledTab = {\n  render: () => (\n    <div style={{ width: '350px' }}>\n      <Tabs defaultValue=\"available\">\n        <TabsList>\n          <TabsTrigger value=\"available\">Available</TabsTrigger>\n          <TabsTrigger value=\"disabled\" disabled>\n            Disabled\n          </TabsTrigger>\n          <TabsTrigger value=\"another\">Another</TabsTrigger>\n        </TabsList>\n        <TabsContent value=\"available\">\n          <Typography variant=\"body1\">This tab is available and active.</Typography>\n        </TabsContent>\n        <TabsContent value=\"disabled\">\n          <Typography variant=\"body1\">This content should not be visible.</Typography>\n        </TabsContent>\n        <TabsContent value=\"another\">\n          <Typography variant=\"body1\">This is another available tab.</Typography>\n        </TabsContent>\n      </Tabs>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Tabs/Tabs.tsx",
    "content": "import { clsx } from 'clsx'\nimport { type ComponentProps, createContext, type ReactNode, use, useState } from 'react'\n\nimport s from './Tabs.module.css'\n\ntype TabsContextType = {\n  value?: string\n  onValueChange?: (value: string) => void\n}\n\nconst TabsContext = createContext<TabsContextType | null>(null)\n\nconst useTabsContext = () => {\n  const context = use(TabsContext)\n  if (!context) {\n    throw new Error('Tabs compound components must be used within Tabs component')\n  }\n  return context\n}\n\n/*\n * Tabs\n */\n\nexport type TabsProps = {\n  children: ReactNode\n  defaultValue?: string\n  value?: string\n  onValueChange?: (value: string) => void\n} & ComponentProps<'div'>\n\nexport const Tabs = ({\n  children,\n  defaultValue,\n  value: controlledValue,\n  onValueChange,\n  className,\n  ...props\n}: TabsProps) => {\n  const [internalValue, setInternalValue] = useState(defaultValue)\n\n  const isControlled = controlledValue !== undefined\n  const value = isControlled ? controlledValue : internalValue\n\n  const handleValueChange = (newValue: string) => {\n    if (!isControlled) {\n      setInternalValue(newValue)\n    }\n    onValueChange?.(newValue)\n  }\n\n  return (\n    <div className={className} {...props}>\n      <TabsContext value={{ value, onValueChange: handleValueChange }}>{children}</TabsContext>\n    </div>\n  )\n}\n\n/*\n * TabsList\n */\n\nexport type TabsListProps = {\n  children: ReactNode\n  className?: string\n}\n\nexport const TabsList = ({ children, className }: TabsListProps) => {\n  return <div className={clsx(s.tabsList, className)}>{children}</div>\n}\n\n/*\n * TabsTrigger\n */\n\nexport type TabsTriggerProps = {\n  children: ReactNode\n  value: string\n  className?: string\n  disabled?: boolean\n}\n\nexport const TabsTrigger = ({ children, value, className, disabled }: TabsTriggerProps) => {\n  const { value: activeValue, onValueChange } = useTabsContext()\n  const isActive = activeValue === value\n\n  const handleClick = () => {\n    if (!disabled) {\n      onValueChange?.(value)\n    }\n  }\n\n  return (\n    <button\n      className={clsx(s.tabsTrigger, isActive && s.active, disabled && s.disabled, className)}\n      onClick={handleClick}\n      disabled={disabled}\n      type=\"button\">\n      {children}\n    </button>\n  )\n}\n\n/*\n * TabsContent\n */\n\nexport type TabsContentProps = {\n  children: ReactNode\n  value: string\n  className?: string\n}\n\nexport const TabsContent = ({ children, value, className }: TabsContentProps) => {\n  const { value: activeValue } = useTabsContext()\n  const isActive = activeValue === value\n\n  if (!isActive) return null\n\n  return <div className={clsx(s.tabsContent, className)}>{children}</div>\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Tabs/index.ts",
    "content": "export * from './Tabs'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/TagEditor/TagEditor.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n}\n\n.tagsContainer {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n\n  margin-top: 12px;\n  padding: 8px 0;\n}\n\n.tag {\n  display: flex;\n  gap: 6px;\n  align-items: center;\n\n  padding: 4px 8px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 16px;\n\n  background-color: var(--color-bg-secondary);\n\n  transition: all 200ms ease;\n}\n\n.tag:hover {\n  background-color: var(--color-bg-input-hover);\n}\n\n.tagText {\n  font-size: var(--font-size-s);\n  font-weight: 500;\n  color: var(--color-text-primary);\n  white-space: nowrap;\n}\n\n.deleteButton {\n  width: 16px;\n  height: 16px;\n  padding: 0;\n\n  font-size: 10px;\n  color: var(--color-text-secondary);\n\n  background: transparent;\n\n  transition: all 200ms ease;\n}\n\n.deleteButton:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.deleteButton:enabled:hover {\n  color: var(--color-text-error);\n  background-color: transparent;\n}\n\n.counter {\n  margin-top: 8px;\n  color: var(--color-text-secondary);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/TagEditor/TagEditor.stories.tsx",
    "content": "import type { Meta } from '@storybook/react-vite'\nimport { useState } from 'react'\n\nimport { Card } from '../Card'\nimport { Typography } from '../Typography'\nimport { TagEditor } from './TagEditor'\n\nconst meta = {\n  title: 'Components/TagEditor',\n  component: TagEditor,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TagEditor>\n\nexport default meta\n\nexport const Basic = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags\"\n          placeholder=\"Add tag and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n        />\n      </div>\n    )\n  },\n}\n\nexport const WithMaxTags = {\n  render: () => {\n    const [tags, setTags] = useState<string[]>([])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Skills (max 5)\"\n          placeholder=\"Add skill and press Enter\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={5}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Disabled = {\n  render: () => {\n    const [tags, setTags] = useState(['React', 'TypeScript'])\n\n    return (\n      <div style={{ width: '400px' }}>\n        <TagEditor\n          label=\"Tags (disabled)\"\n          placeholder=\"Cannot add tags\"\n          value={tags}\n          onTagsChange={setTags}\n          disabled={true}\n        />\n      </div>\n    )\n  },\n}\n\nexport const PrefilledTags = {\n  render: () => {\n    const [tags, setTags] = useState([\n      'JavaScript',\n      'TypeScript',\n      'React',\n      'Node.js',\n      'CSS',\n      'HTML',\n    ])\n\n    return (\n      <div style={{ width: '450px' }}>\n        <TagEditor\n          label=\"Programming Languages & Technologies\"\n          placeholder=\"Add more technologies...\"\n          value={tags}\n          onTagsChange={setTags}\n          maxTags={10}\n        />\n      </div>\n    )\n  },\n}\n\nexport const Interactive = {\n  render: () => {\n    const [frontendTags, setFrontendTags] = useState(['React', 'Vue.js'])\n    const [backendTags, setBackendTags] = useState(['Node.js'])\n\n    return (\n      <div\n        style={{\n          width: '500px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '24px',\n        }}>\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Frontend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Frontend\"\n            placeholder=\"Add frontend technology...\"\n            value={frontendTags}\n            onTagsChange={setFrontendTags}\n            maxTags={8}\n          />\n        </div>\n\n        <div>\n          <Typography variant=\"h3\" style={{ marginBottom: '16px' }}>\n            Backend Technologies\n          </Typography>\n          <TagEditor\n            label=\"Backend\"\n            placeholder=\"Add backend technology...\"\n            value={backendTags}\n            onTagsChange={setBackendTags}\n            maxTags={6}\n          />\n        </div>\n\n        <Card>\n          <Typography variant=\"body2\" style={{ marginBottom: '8px' }}>\n            Summary:\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block', marginBottom: '4px' }}>\n            Frontend: {frontendTags.length > 0 ? frontendTags.join(', ') : 'None'}\n          </Typography>\n          <Typography variant=\"caption\" style={{ display: 'block' }}>\n            Backend: {backendTags.length > 0 ? backendTags.join(', ') : 'None'}\n          </Typography>\n        </Card>\n      </div>\n    )\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/TagEditor/TagEditor.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, KeyboardEvent } from 'react'\nimport { useState } from 'react'\n\nimport { DeleteIcon } from '@/shared/icons'\n\nimport { IconButton } from '../IconButton'\nimport { TextField } from '../TextField'\nimport { Typography } from '../Typography'\nimport s from './TagEditor.module.css'\n\nexport type TagEditorProps = {\n  label?: string\n  placeholder?: string\n  value: string[]\n  onTagsChange: (tags: string[]) => void\n  maxTags?: number\n  disabled?: boolean\n} & ComponentProps<'div'>\n\nexport const TagEditor = ({\n  label,\n  placeholder = 'Add tag and press Enter',\n  value,\n  onTagsChange,\n  className,\n  maxTags,\n  disabled = false,\n  ...props\n}: TagEditorProps) => {\n  const [inputValue, setInputValue] = useState('')\n\n  const addTag = (tag: string) => {\n    const trimmedTag = tag.trim()\n\n    if (!trimmedTag) return\n    if (value.includes(trimmedTag)) return\n    if (maxTags && value.length >= maxTags) return\n\n    onTagsChange([...value, trimmedTag])\n    setInputValue('')\n  }\n\n  const removeTag = (tagToRemove: string) => {\n    onTagsChange(value.filter((tag) => tag !== tagToRemove))\n  }\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      addTag(inputValue)\n    }\n\n    if (e.key === 'Backspace' && !inputValue && value.length > 0) {\n      removeTag(value[value.length - 1])\n    }\n  }\n\n  const isMaxTagsReached = maxTags ? value.length >= maxTags : false\n\n  return (\n    <div className={clsx(s.container, className)} {...props}>\n      <TextField\n        label={label}\n        value={inputValue}\n        onChange={(e) => setInputValue(e.target.value)}\n        onKeyDown={handleKeyDown}\n        placeholder={isMaxTagsReached ? 'Max tags reached' : placeholder}\n        disabled={disabled}\n      />\n\n      {value.length > 0 && (\n        <ul className={s.tagsContainer}>\n          {value.map((tag) => (\n            <li key={tag} className={s.tag}>\n              <Typography variant=\"body2\" className={s.tagText}>\n                {tag}\n              </Typography>\n              <IconButton\n                onClick={() => removeTag(tag)}\n                className={s.deleteButton}\n                disabled={disabled}\n                aria-label={`Remove tag ${tag}`}\n                type=\"button\">\n                <DeleteIcon />\n              </IconButton>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {maxTags && (\n        <Typography variant=\"caption\" className={s.counter}>\n          {value.length}/{maxTags} tags\n        </Typography>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/TagEditor/index.ts",
    "content": "export * from './TagEditor'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/TextField/TextField.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.inputWrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.icon {\n  position: absolute;\n  top: 50%;\n  left: 12px;\n  transform: translateY(-50%);\n\n  display: flex;\n\n  color: var(--color-text-secondary);\n}\n\n.input {\n  width: 100%;\n  height: 40px;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.input.large {\n  height: 56px;\n}\n\n.input:disabled {\n  color: var(--color-disabled);\n}\n\n.input:focus,\n.input:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.input:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.input::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.input.error {\n  border-color: var(--color-text-error);\n}\n\n.input.withIcon {\n  padding-left: 40px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/TextField/TextField.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { SearchIcon } from '@/shared/icons'\n\nimport { TextField } from './TextField'\n\nconst meta = {\n  title: 'Components/TextField',\n  component: TextField,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof TextField>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const Search: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    icon: <SearchIcon width={20} height={20} />,\n    inputSize: 'l',\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/TextField/TextField.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './TextField.module.css'\n\nexport type TextFieldSize = 'm' | 'l'\n\nexport type TextFieldProps = {\n  errorMessage?: string\n  label?: ReactNode\n  icon?: ReactNode\n  inputSize?: TextFieldSize\n} & ComponentProps<'input'>\n\nexport const TextField = ({\n  className,\n  errorMessage,\n  id,\n  icon,\n  label,\n  inputSize = 'm',\n  ...props\n}: TextFieldProps) => {\n  const showError = Boolean(errorMessage)\n  const inputId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={inputId}>\n          {label}\n        </Typography>\n      )}\n\n      <div className={s.inputWrapper}>\n        {icon && <span className={s.icon}>{icon}</span>}\n        <input\n          className={clsx(\n            s.input,\n            showError && s.error,\n            icon && s.withIcon,\n            inputSize === 'l' && s.large\n          )}\n          id={inputId}\n          type={'text'}\n          {...props}\n        />\n      </div>\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/TextField/index.ts",
    "content": "export * from './TextField'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Textarea/Textarea.module.css",
    "content": ".box {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.textarea {\n  resize: none;\n\n  width: 100%;\n  padding: 8px 12px;\n  border: 1px solid var(--color-border-input-primary);\n  border-radius: 4px;\n\n  font-size: var(--font-size-m);\n  color: var(--color-text-primary);\n\n  background-color: var(--color-bg-primary);\n  outline: none;\n\n  transition:\n    200ms background-color,\n    200ms color,\n    200ms border-color;\n}\n\n.textarea:disabled {\n  color: var(--color-disabled);\n}\n\n.textarea:focus,\n.textarea:active:enabled {\n  border-color: var(--color-border-input-active);\n}\n\n.textarea:hover:not(:disabled) {\n  background-color: var(--color-bg-input-hover);\n}\n\n.textarea::placeholder {\n  color: var(--color-text-secondary);\n}\n\n.textarea.error {\n  border-color: var(--color-text-error);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Textarea/Textarea.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Textarea } from './Textarea'\n\nconst meta = {\n  title: 'Components/Textarea',\n  component: Textarea,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Textarea>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Primary: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n  },\n}\n\nexport const Disabled: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    disabled: true,\n  },\n}\n\nexport const Error: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    errorMessage: 'Some error message',\n  },\n}\n\nexport const WithRows: Story = {\n  args: {\n    label: 'Some label',\n    placeholder: 'Some placeholder',\n    rows: 5,\n  },\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Textarea/Textarea.tsx",
    "content": "import { clsx } from 'clsx'\nimport type { ComponentProps, ReactNode } from 'react'\n\nimport { useGetId } from '../../hooks/useGetId'\nimport { Typography } from '../Typography'\nimport s from './Textarea.module.css'\n\nexport type TextareaProps = {\n  errorMessage?: string\n  label?: ReactNode\n} & ComponentProps<'textarea'>\n\nexport const Textarea = ({ className, errorMessage, id, label, ...props }: TextareaProps) => {\n  const showError = Boolean(errorMessage)\n  const textareaId = useGetId(id)\n\n  return (\n    <div className={clsx(s.box, className)}>\n      {label && (\n        <Typography variant=\"label\" as=\"label\" htmlFor={textareaId}>\n          {label}\n        </Typography>\n      )}\n\n      <textarea className={clsx(s.textarea, showError && s.error)} id={textareaId} {...props} />\n\n      {showError && <Typography variant=\"error\">{errorMessage}</Typography>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Textarea/index.ts",
    "content": "export * from './Textarea'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Typography/Typography.module.css",
    "content": ".label {\n  font-size: var(--font-size-s);\n  line-height: 1.7;\n  color: var(--color-text-label);\n}\n\n.error {\n  font-size: var(--font-size-s);\n  color: var(--color-text-error);\n}\n\n.h1 {\n  font-size: var(--font-size-xxxl);\n}\n\n.h2 {\n  margin: 0;\n  font-size: var(--font-size-xl);\n  font-weight: 600;\n  line-height: 1.3;\n}\n\n.h3 {\n  margin: 0;\n  font-size: var(--font-size-xs);\n  font-weight: 600;\n  line-height: 1.7;\n}\n\n.body1 {\n  margin: 0;\n  font-size: var(--font-size-l);\n  font-weight: 400;\n}\n\n.body2 {\n  margin: 0;\n  font-size: var(--font-size-m);\n  font-weight: 400;\n  color: var(--color-text-secondary);\n}\n\n.body3 {\n  margin: 0;\n  font-size: var(--font-size-xxs);\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n/* ------------------------------------------------------------ */\n\n.caption {\n  margin: 0;\n  font-size: 0.75rem;\n  font-weight: 400;\n  line-height: 1.66;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Typography/Typography.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Typography } from './Typography'\n\nconst meta = {\n  title: 'Components/Typography',\n  component: Typography,\n  parameters: {\n    layout: 'centered',\n  },\n  args: {},\n} satisfies Meta<typeof Typography>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const AllTypography: Story = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>\n      <Typography variant=\"h1\">h1</Typography>\n      <Typography variant=\"h2\">h2</Typography>\n      <Typography variant=\"h3\">h3</Typography>\n      <Typography variant=\"body1\">body1</Typography>\n      <Typography variant=\"body2\">body2</Typography>\n      <Typography variant=\"caption\">caption</Typography>\n      <Typography variant=\"label\">label</Typography>\n      <Typography variant=\"error\">error</Typography>\n    </div>\n  ),\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Typography/Typography.tsx",
    "content": "import clsx from 'clsx'\nimport type { ComponentProps, ElementType } from 'react'\nimport React from 'react'\n\nimport styles from './Typography.module.css'\n\nconst VARIANT_DEFAULT_COMPONENT: Record<string, ElementType> = {\n  h1: 'h1',\n  h2: 'h2',\n  h3: 'h3',\n  body1: 'p',\n  body2: 'p',\n  body3: 'p',\n  caption: 'span',\n  label: 'label',\n}\n\ntype TypographyVariant =\n  | 'h1'\n  | 'h2'\n  | 'h3'\n  | 'body1'\n  | 'body2'\n  | 'body3'\n  | 'caption'\n  | 'label'\n  | 'error'\n\ntype Props<T extends ElementType> = {\n  variant?: TypographyVariant\n  as?: T\n  children: React.ReactNode\n} & ComponentProps<T>\n\nexport const Typography = <T extends ElementType = 'span'>({\n  variant = 'body1',\n  as,\n  children,\n  className = '',\n  ...props\n}: Props<T>) => {\n  const Component = as || VARIANT_DEFAULT_COMPONENT[variant] || 'span'\n  const variantClass = styles[variant] || ''\n\n  return (\n    <Component className={clsx(variantClass, className)} {...props}>\n      {children}\n    </Component>\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/Typography/index.ts",
    "content": "export * from './Typography'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/components/index.ts",
    "content": "export * from './AudioPlayer'\nexport * from './Autocomplete'\nexport * from './Button'\nexport * from './Card'\nexport * from './Dialog'\nexport * from './DropdownMenu'\nexport * from './Hashtag'\nexport * from './IconButton'\nexport * from './ImageUploader'\nexport * from './Pagination'\nexport * from './Progress'\nexport * from './ReactionButtons'\nexport * from './SearchField'\nexport * from './Select'\nexport * from './Table'\nexport * from './Tabs'\nexport * from './TagEditor'\nexport * from './Textarea'\nexport * from './TextField'\nexport * from './Typography'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/hooks/index.ts",
    "content": "export * from './useGetId'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/hooks/useGetId.ts",
    "content": "import { useId } from 'react'\n\n/*\n * Custom hook to get an ID.\n * If an ID is passed from component props, it returns that ID.\n * Otherwise, it generates and returns a new unique ID.\n *\n * @param {string} [idFromComponentProps] - An optional ID passed from ComponentProps.\n * @returns {string} The ID from component props or a generated unique ID.\n */\nexport const useGetId = (idFromComponentProps?: string) => {\n  const generatedId = useId()\n\n  return idFromComponentProps || generatedId\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/AddToPlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const AddToPlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <circle cx={7.891} cy={7} r={5.5} fill=\"currentColor\" />\n    <path\n      fill=\"#000\"\n      d=\"M8.134 4.795v2.456h2.34v.776h-2.34V10.5h-.84V8.026H4.966v-.776h2.328V4.795h.84Z\"\n    />\n    <path\n      fill=\"#fff\"\n      d=\"M5.89 16.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.665 2.333 2.333 0 0 0 0-4.665ZM17.89 14.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 1.167a2.333 2.333 0 1 0 0 4.666 2.333 2.333 0 0 0 0-4.666ZM10.902 5.9l10.489-1.998v1l-10.5 2 .011-1.003Z\"\n    />\n    <path fill=\"#fff\" d=\"M8.39 11.5h1v8l-1-.533V11.5ZM20.39 4.964l1-.464v13l-1-.928V4.963Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/ArrowDownIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ArrowDownIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={20}\n    height={20}\n    viewBox=\"0 0 20 20\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M6.175 7.158 10 10.975l3.825-3.817L15 8.333l-5 5-5-5 1.175-1.175Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/ClockIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ClockIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    viewBox=\"0 0 28 28\"\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14 3c6.075 0 11 4.925 11 11s-4.925 11-11 11S3 20.075 3 14 7.925 3 14 3Zm0 2a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm.5 8.5H18v2h-5.5v-7h2v5Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M0 0h28v28H0z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/CreateIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const CreateIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M16 2.667C8.64 2.667 2.667 8.64 2.667 16S8.64 29.333 16 29.333 29.333 23.36 29.333 16 23.36 2.666 16 2.666Zm6.667 14.666h-5.334v5.334h-2.666v-5.334H9.333v-2.666h5.334V9.332h2.666v5.333h5.334v2.667Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/DeleteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DeleteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={10}\n    height={12}\n    viewBox=\"0 0 10 12\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M7.333 4.25v5.833H2.666V4.25h4.667ZM6.458.75H3.54l-.583.583H.916V2.5h8.167V1.333H7.04L6.458.75Zm2.041 2.333h-7v7a1.17 1.17 0 0 0 1.167 1.167h4.667a1.17 1.17 0 0 0 1.166-1.167v-7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/DislikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DislikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 28 28\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-1.12 0-2.217.292-3.185.805L14 10.5h3.5L14 22.167l1.167-10.5h-3.5l1.796-6.289C12.215 4.212 10.512 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.818 4.854 8.376 11.667 14.583 6.382-5.763 11.667-9.637 11.667-14.583 0-3.594-2.824-6.417-6.417-6.417Zm-7.303 16.018c-4.422-3.955-7.28-6.685-7.28-9.601A4.044 4.044 0 0 1 8.75 5.833c.688 0 1.388.175 2.018.49L8.575 14h3.99l-.618 5.518Zm5.705-1.4 2.986-9.951h-3.395l.712-2.124c.42-.14.863-.21 1.295-.21a4.044 4.044 0 0 1 4.083 4.084c0 2.578-2.356 5.168-5.681 8.201Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/DownloadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const DownloadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={22}\n    height={22}\n    viewBox=\"0 0 22 22\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M11.733 14.164V5.867h-1.466v8.286l-2.822-3.28-1.112.954 4.668 5.43 4.687-5.427-1.112-.958-2.843 3.292ZM11 0C4.925 0 0 4.925 0 11s4.925 11 11 11 11-4.925 11-11S17.075 0 11 0Zm0 20.533c-5.257 0-9.533-4.277-9.533-9.533 0-5.257 4.276-9.533 9.533-9.533 5.256 0 9.533 4.276 9.533 9.533 0 5.256-4.277 9.533-9.533 9.533Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/EditIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const EditIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m13.888 9.517.844.766-8.305 7.55h-.844v-.766l8.305-7.55Zm3.3-5.017a.966.966 0 0 0-.641.242l-1.678 1.525 3.438 3.125 1.677-1.525a.778.778 0 0 0 0-1.175l-2.145-1.95a.949.949 0 0 0-.65-.242Zm-3.3 2.658L3.75 16.375V19.5h3.438l10.138-9.217-3.438-3.125Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/HomeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const HomeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      d=\"M16.0001 7.58667L22.6667 13.5867V24H20.0001V16H12.0001V24H9.33341V13.5867L16.0001 7.58667ZM16.0001 4L2.66675 16H6.66675V26.6667H14.6667V18.6667H17.3334V26.6667H25.3334V16H29.3334L16.0001 4Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/ImageUploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ImageUploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={35}\n    height={34}\n    fill=\"none\"\n    viewBox=\"0 0 35 34\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M30.834 3.667v20h-20v-20h20Zm0-3.334h-20a3.343 3.343 0 0 0-3.333 3.334v20C7.5 25.5 9 27 10.834 27h20c1.833 0 3.333-1.5 3.333-3.333v-20c0-1.834-1.5-3.334-3.333-3.334ZM16.667 16.45l2.817 3.767 4.133-5.167 5.55 6.95H12.501l4.166-5.55ZM.834 7v23.333c0 1.834 1.5 3.334 3.333 3.334h23.334v-3.334H4.167V7H.834Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/KeyboardArrowLeftIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowLeftIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path fill=\"currentColor\" d=\"M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/KeyboardArrowRightIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const KeyboardArrowRightIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width={24} height={24} fill=\"none\" {...props}>\n    <path fill=\"#fff\" d=\"M8.59 16.59 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41Z\" />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/LibraryIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LibraryIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}>\n    <path\n      fill=\"  currentColor\"\n      d=\"M26.667 2.667h-16A2.674 2.674 0 0 0 8 5.332v16C8 22.8 9.2 24 10.667 24h16c1.466 0 2.666-1.2 2.666-2.667v-16c0-1.467-1.2-2.667-2.666-2.667Zm0 16.666a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12Zm-10 .667A3.335 3.335 0 0 0 20 16.666v-5.333a2 2 0 0 1 2-2h.667a1.333 1.333 0 1 0 0-2.667h-2a2 2 0 0 0-2 2v3.196c0 .882-1.119 1.471-2 1.471a3.334 3.334 0 0 0 0 6.667ZM5.333 9.333a1.333 1.333 0 1 0-2.666 0v17.333c0 1.467 1.2 2.667 2.666 2.667h17.334a1.333 1.333 0 0 0 0-2.666H7.333a2 2 0 0 1-2-2V9.332Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/LikeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={28}\n    height={28}\n    fill=\"none\"\n    viewBox=\"0 0 28 28\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19.25 3.5c-2.03 0-3.978.945-5.25 2.438C12.728 4.445 10.78 3.5 8.75 3.5c-3.593 0-6.417 2.823-6.417 6.417 0 4.41 3.967 8.003 9.975 13.463L14 24.908l1.692-1.54c6.008-5.448 9.975-9.041 9.975-13.451 0-3.594-2.824-6.417-6.417-6.417Zm-5.133 18.142-.117.116-.117-.116C8.33 16.613 4.667 13.288 4.667 9.917c0-2.334 1.75-4.084 4.083-4.084 1.797 0 3.547 1.155 4.165 2.754h2.182c.606-1.599 2.356-2.754 4.153-2.754 2.333 0 4.083 1.75 4.083 4.084 0 3.371-3.663 6.696-9.216 11.725Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/LikeIconFill.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeIconFill = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 29 28\"\n    width={29}\n    height={28}\n    fill=\"none\"\n    {...props}>\n    <g clipPath=\"url(#a)\">\n      <path\n        fill=\"currentColor\"\n        d=\"M14.4 6.04a6.137 6.137 0 0 1 8.655.248c2.375 2.47 2.457 6.402.247 8.967L14.4 24.5l-8.902-9.245c-2.21-2.566-2.126-6.504.248-8.967C8.123 3.823 11.927 3.74 14.4 6.04Z\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"currentColor\" d=\"M.4 0h28v28H.4z\" />\n      </clipPath>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/LikeInSquareIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const LikeInSquareIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <rect width={32} height={32} fill=\"url(#a)\" rx={2} />\n    <path\n      fill=\"#fff\"\n      d=\"M16 10.158c1.645-1.597 4.186-1.544 5.77.173 1.583 1.717 1.638 4.453.165 6.237L16 23l-5.934-6.432c-1.473-1.784-1.418-4.524.165-6.237 1.585-1.715 4.121-1.773 5.77-.173Z\"\n    />\n    <defs>\n      <linearGradient id=\"a\" x1={1} x2={32} y1={1} y2={30.5} gradientUnits=\"userSpaceOnUse\">\n        <stop stopColor=\"#3822EA\" />\n        <stop offset={1} stopColor=\"#C7E9D7\" />\n      </linearGradient>\n    </defs>\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/LiveWaveIcon/LiveWaveIcon.module.css",
    "content": ".bar {\n  transform-origin: center bottom;\n  animation: wave 1.2s ease-in-out infinite alternate;\n}\n\n@keyframes wave {\n  0% {\n    transform: scaleY(0.4);\n  }\n\n  50% {\n    transform: scaleY(1);\n  }\n\n  100% {\n    transform: scaleY(0.6);\n  }\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/LiveWaveIcon/LiveWaveIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nimport s from './LiveWaveIcon.module.css'\n\nexport const LiveWaveIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <rect\n      x={2}\n      y={8}\n      width={2}\n      height={8}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '0ms' }}\n    />\n    <rect\n      x={6}\n      y={4}\n      width={2}\n      height={16}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '150ms' }}\n    />\n    <rect\n      x={10}\n      y={6}\n      width={2}\n      height={12}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '300ms' }}\n    />\n    <rect\n      x={14}\n      y={2}\n      width={2}\n      height={20}\n      rx={1}\n      fill=\"currentColor\"\n      className={s.bar}\n      style={{ animationDelay: '450ms' }}\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/LiveWaveIcon/index.ts",
    "content": "export { LiveWaveIcon } from './LiveWaveIcon'\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/LogoutIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const LogoutIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m17 8-1.41 1.41L17.17 11H9v2h8.17l-1.58 1.58L17 16l4-4-4-4ZM5 5h7V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h7v-2H5V5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/MoreIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const MoreIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={16}\n    height={4}\n    viewBox=\"0 0 16 4\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M2 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM8 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM16 2a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/PauseIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PauseIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 40 40\"\n    width={40}\n    height={40}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"#fff\"\n      d=\"M20 0c11.046 0 20 8.954 20 20s-8.954 20-20 20S0 31.046 0 20 8.954 0 20 0Zm-6 11a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Zm9 0a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V12a1 1 0 0 0-1-1h-3Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/PlayIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlayIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={72}\n    height={72}\n    viewBox=\"0 0 72 72\"\n    fill=\"none\"\n    {...props}>\n    <circle cx={36} cy={36} r={36} fill=\"#FF38B6\" />\n    <path\n      fill=\"#000\"\n      d=\"M49.287 36.512c.865-.486.865-1.7 0-2.186l-19.47-10.93c-.864-.485-1.946.122-1.946 1.093v21.86c0 .971 1.082 1.579 1.947 1.093l19.469-10.93Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/PlaylistIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlaylistIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M28 4H4a2.675 2.675 0 0 0-2.667 2.667v18.666C1.333 26.8 2.533 28 4 28h24c1.467 0 2.667-1.2 2.667-2.667V6.667C30.667 5.2 29.467 4 28 4Zm0 21.333H4V6.667h24v18.666ZM10.667 20c0-2.213 1.786-4 4-4 .466 0 .92.093 1.333.24V8h6.667v2.667h-4v9.373a4.003 4.003 0 0 1-4 3.96c-2.214 0-4-1.787-4-4Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/PlusIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const PlusIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"currentColor\"\n    viewBox=\"0 0 32 32\"\n    {...props}>\n    <path\n      fill=\"var(--color-text-secondary)\"\n      d=\"M30 0a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h28ZM15 9v6H9v2h6v6h2v-6h6v-2h-6V9h-2Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/ProfileIcon.tsx",
    "content": "import { type SVGProps } from 'react'\n\nexport const ProfileIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M19 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2Zm0 16h-4.83L12 20.17 9.83 18H5V4h14v14Zm-7-7c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3Zm0-4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1Zm6 8.58c0-2.5-3.97-3.58-6-3.58s-6 1.08-6 3.58V17h12v-1.42ZM8.48 15c.74-.51 2.23-1 3.52-1s2.78.49 3.52 1H8.48Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/RepeatIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const RepeatIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M9.333 9.333h13.334v4L28 8l-5.333-5.333v4h-16v8h2.666V9.332Zm13.334 13.333H9.333v-4L4 24l5.333 5.333v-4h16v-8h-2.666v5.334Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/SearchIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SearchIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 32 32\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m23.775 22.356 5.817 6.137c.56.59.541 1.534-.04 2.1a1.414 1.414 0 0 1-2.024-.04l-5.822-6.145c-1.979 1.522-4.21 2.36-6.695 2.512a11.872 11.872 0 0 1-4.822-.691c-1.556-.563-2.912-1.366-4.07-2.41-1.159-1.042-2.107-2.313-2.843-3.813a12.37 12.37 0 0 1-1.254-4.779 12.41 12.41 0 0 1 .687-4.898c.557-1.58 1.35-2.958 2.378-4.136 1.028-1.177 2.281-2.14 3.76-2.89a11.915 11.915 0 0 1 4.707-1.28c1.66-.102 3.268.129 4.823.692 1.555.563 2.912 1.366 4.07 2.409 1.159 1.043 2.106 2.314 2.843 3.814a12.368 12.368 0 0 1 1.253 4.779 12.567 12.567 0 0 1-.21 3.162 12.259 12.259 0 0 1-.958 2.929 12.892 12.892 0 0 1-1.6 2.548Zm-8.935 1.635a9.024 9.024 0 0 0 3.596-.982 9.525 9.525 0 0 0 2.869-2.216c.786-.9 1.394-1.952 1.823-3.156a9.4 9.4 0 0 0 .53-3.743 9.367 9.367 0 0 0-.963-3.65c-.566-1.143-1.292-2.113-2.178-2.91a9.443 9.443 0 0 0-3.106-1.847 8.992 8.992 0 0 0-3.685-.534 9.025 9.025 0 0 0-3.596.982A9.524 9.524 0 0 0 7.26 8.15c-.785.9-1.393 1.953-1.822 3.157a9.4 9.4 0 0 0-.53 3.742 9.367 9.367 0 0 0 .962 3.65c.567 1.144 1.293 2.114 2.179 2.91a9.443 9.443 0 0 0 3.106 1.848 8.994 8.994 0 0 0 3.685.534Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/ShuffleIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const ShuffleIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M14.12 12.227 7.213 5.333l-1.88 1.88 6.893 6.894 1.894-1.88Zm5.213-6.894 2.72 2.72-16.72 16.734 1.88 1.88 16.733-16.72 2.72 2.72V5.334h-7.333Zm.44 12.547-1.88 1.88 4.173 4.173-2.733 2.734h7.333v-7.334l-2.72 2.72-4.173-4.173Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/SkipNextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipNextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"m8 24 11.333-8L8 8v16Zm2.667-10.853L14.707 16l-4.04 2.853v-5.706ZM21.333 8H24v16h-2.667V8Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/SkipPreviousIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const SkipPreviousIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      fillOpacity={0.7}\n      d=\"M8 8h2.667v16H8V8Zm4.667 8L24 24V8l-11.333 8Zm8.666 2.853L17.293 16l4.04-2.853v5.706Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/TextIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TextIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={24}\n    height={24}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M14.17 5 19 9.83V19H5V5h9.17Zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V9.83c0-.53-.21-1.04-.59-1.41l-4.83-4.83c-.37-.38-.88-.59-1.41-.59ZM7 15h10v2H7v-2Zm0-4h10v2H7v-2Zm0-4h7v2H7V7Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/TrackIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const TrackIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"m16 4 .013 14.067a5.329 5.329 0 0 0-2.666-.734A5.335 5.335 0 0 0 8 22.667 5.335 5.335 0 0 0 13.347 28c2.96 0 5.32-2.387 5.32-5.333V9.333H24V4h-8Zm-2.653 21.333a2.674 2.674 0 0 1-2.667-2.666c0-1.467 1.2-2.667 2.667-2.667 1.466 0 2.666 1.2 2.666 2.667 0 1.466-1.2 2.666-2.666 2.666Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/UploadIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const UploadIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M24 20v4H8v-4H5.333v4c0 1.467 1.2 2.667 2.667 2.667h16c1.467 0 2.667-1.2 2.667-2.667v-4H24ZM9.333 12l1.88 1.88 3.454-3.44v10.894h2.666V10.44l3.454 3.44 1.88-1.88L16 5.333 9.333 12Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/VolumeIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 32 32\"\n    width={32}\n    height={32}\n    fill=\"none\"\n    {...props}>\n    <path\n      fill=\"currentColor\"\n      d=\"M4 12v8h5.333L16 26.667V5.333L9.333 12H4Zm9.333-.227v8.454l-2.893-2.894H6.667v-2.666h3.773l2.893-2.894ZM22 16a6 6 0 0 0-3.333-5.373V21.36A5.965 5.965 0 0 0 22 16ZM18.667 4.307v2.746C22.52 8.2 25.333 11.773 25.333 16c0 4.227-2.813 7.8-6.666 8.947v2.746C24.013 26.48 28 21.707 28 16S24.013 5.52 18.667 4.307Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/VolumeMuteIcon.tsx",
    "content": "import type { SVGProps } from 'react'\n\nexport const VolumeMuteIcon = (props: SVGProps<SVGSVGElement>) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width={24} height={24} {...props}>\n    <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n    <g fill=\"currentColor\">\n      <path d=\"M16.25 13.42c.15-.45.25-.92.25-1.42A4.5 4.5 0 0 0 14 7.97v3.2l2.25 2.25z\" />\n      <path d=\"M19 12c0 1.21-.31 2.34-.85 3.32l1.46 1.46A8.973 8.973 0 0 0 21 12c0-3.83-2.4-7.11-5.78-8.4-.59-.23-1.22.23-1.22.86v.19c0 .38.25.71.61.85C17.18 6.54 19 9.06 19 12zM2.1 3.51a.996.996 0 0 0 0 1.41L6.17 9H4c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71v-2.76l3.32 3.32c-.23.13-.47.24-.71.35-.37.16-.6.52-.6.91 0 .7.7 1.2 1.35.94.5-.2.99-.45 1.44-.73l2.28 2.28a.996.996 0 1 0 1.41-1.41L3.51 3.51a.996.996 0 0 0-1.41 0zM12 9.17V6.41c0-.89-1.08-1.34-1.71-.71l-.88.89L12 9.17z\" />\n    </g>\n  </svg>\n)\n"
  },
  {
    "path": "apps/ui-vanilla/src/shared/icons/index.ts",
    "content": "export * from './AddToPlaylistIcon'\nexport * from './ArrowDownIcon'\nexport * from './ClockIcon'\nexport * from './CreateIcon'\nexport * from './DeleteIcon'\nexport * from './DislikeIcon'\nexport * from './DownloadIcon'\nexport * from './EditIcon'\nexport * from './HomeIcon'\nexport * from './ImageUploadIcon'\nexport * from './KeyboardArrowLeftIcon'\nexport * from './KeyboardArrowRightIcon'\nexport * from './LibraryIcon'\nexport * from './LikeIcon'\nexport * from './LikeIconFill'\nexport * from './LikeInSquareIcon'\nexport * from './LiveWaveIcon'\nexport * from './LogoutIcon'\nexport * from './MoreIcon'\nexport * from './PauseIcon'\nexport * from './PlayIcon'\nexport * from './PlaylistIcon'\nexport * from './PlusIcon'\nexport * from './ProfileIcon'\nexport * from './RepeatIcon'\nexport * from './SearchIcon'\nexport * from './ShuffleIcon'\nexport * from './SkipNextIcon'\nexport * from './SkipPreviousIcon'\nexport * from './TextIcon'\nexport * from './TrackIcon'\nexport * from './UploadIcon'\nexport * from './VolumeIcon'\nexport * from './VolumeMuteIcon'\n"
  },
  {
    "path": "apps/ui-vanilla/src/styles/fonts.css",
    "content": "/*\n  source: https://gwfh.mranftl.com/fonts/lato?subsets=latin\n*/\n\n/* lato-regular - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../shared/fonts/lato-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-700 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../shared/fonts/lato-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n\n/* lato-900 - latin */\n@font-face {\n  font-family: Lato;\n  font-weight: 900;\n  font-style: normal;\n  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */\n  src: url('../shared/fonts/lato-v24-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/styles/global.css",
    "content": ":root {\n  font-family: Lato, sans-serif;\n  font-weight: 400;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  line-height: 100%;\n  text-rendering: optimizelegibility;\n\n  font-synthesis: none;\n}\n\n/* Scrollbar styles */\n* {\n  scrollbar-color: var(--color-bg-secondary) var(--color-bg-primary);\n  scrollbar-width: thin;\n}\n\nbody {\n  margin: 0;\n  color: var(--color-text-primary);\n  background-color: var(--color-bg-primary);\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/styles/reset.css",
    "content": "/* Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset */\n\n/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Prevent font size inflation */\nhtml {\n  text-size-adjust: none;\n}\n\n/* Remove default margin in favour of better control in authored CSS */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nfigure,\nblockquote,\ndl,\ndd {\n  margin-block-end: 0;\n}\n\nul,\nol {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  line-height: 1.5;\n}\n\n/* Set shorter line heights on headings and interactive elements */\nh1,\nh2,\nh3,\nh4,\nbutton,\ninput,\nlabel {\n  border: none;\n  line-height: 1.1;\n}\n\n/* Balance text wrapping on headings */\nh1,\nh2,\nh3,\nh4 {\n  text-wrap: balance;\n}\n\n/* A elements that don't have a class get default styles */\na {\n  color: currentcolor;\n  text-decoration: none;\n}\n\n/* Make images easier to work with */\nimg,\npicture {\n  display: block;\n  max-width: 100%;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n/* Anything that has been anchored to should have extra scroll margin */\n:target {\n  scroll-margin-block: 5ex;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/styles/variables.css",
    "content": ":root {\n  /*\n  * Colors\n  */\n  --color-accent: #ff38b6;\n  --color-disabled: #858585;\n  --color-outline-focus: #1a75f5;\n\n  /* Text */\n  --color-text-primary: #fff;\n  --color-text-primary-reverse: #000;\n  --color-text-secondary: #b3b3b3;\n  --color-text-label: #808080;\n  --color-text-error: #f51a51;\n\n  /* Backgrounds */\n  --color-bg-primary: #000;\n  --color-bg-secondary: #141414;\n  --color-bg-primary-reverse: #fff;\n  --color-bg-input-hover: #262626;\n  --color-bg-card: rgb(7 7 7 / 50%);\n  --color-bg-interactive-secondary: #333;\n\n  /* Borders */\n  --color-border-base: #7f7f7f;\n  --color-border-input-primary: #4d4d4d;\n  --color-border-input-active: #fffefe;\n\n  /*\n  * Typography\n  */\n\n  /* font-sizes */\n  --font-size-xxxs: 12px;\n  --font-size-xxs: 13px;\n  --font-size-xs: 14px;\n  --font-size-s: 16px;\n  --font-size-m: 18px;\n  --font-size-l: 20px;\n  --font-size-xl: 24px;\n  --font-size-xxl: 30px;\n  --font-size-xxxl: 60px;\n\n  /*\n  * Layout\n  */\n  --header-height: 80px;\n  --player-height: 112px;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/ui-vanilla/src/widgets/Player/Player.module.css",
    "content": ".player {\n  grid-area: player;\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/widgets/Player/Player.tsx",
    "content": "import { useState } from 'react'\n\nimport { AudioPlayer } from '@/shared/components'\n\nimport s from './Player.module.css'\n\nconst MOCK_TRACK = {\n  src: 'https://cdn.uppbeat.io/audio-files/c636d7c86452449b1203fc0bded83e29/4358717fc9da477a52fb18a6cbd3afcc/d154b5ce5ff1a05ae8115a3c678062e8/STREAMING-dreamland-matrika-main-version-31140-02-25.mp3',\n  cover: 'https://unsplash.it/112/112',\n  title: 'Play It Safe',\n  artist: 'Julia Wolf',\n}\n\nexport const Player = () => {\n  const [isPlaying, setIsPlaying] = useState(false)\n  const [isShuffle, setIsShuffle] = useState(false)\n  const [isRepeat, setIsRepeat] = useState(false)\n\n  return (\n    <AudioPlayer\n      {...MOCK_TRACK}\n      isPlaying={isPlaying}\n      setIsPlaying={setIsPlaying}\n      onNext={() => {}}\n      onPrevious={() => {}}\n      isShuffle={isShuffle}\n      isRepeat={isRepeat}\n      onShuffle={() => setIsShuffle(!isShuffle)}\n      onRepeat={() => setIsRepeat(!isRepeat)}\n      className={s.player}\n    />\n  )\n}\n"
  },
  {
    "path": "apps/ui-vanilla/src/widgets/Player/index.ts",
    "content": "export * from './Player'\n"
  },
  {
    "path": "apps/ui-vanilla/stylelint.config.js",
    "content": "export default {\n  extends: ['stylelint-config-standard', 'stylelint-config-clean-order'],\n  rules: {\n    // Class selector pattern (allow camelCase for CSS modules)\n    'selector-class-pattern': null,\n\n    // Allow unknown at-rules (for CSS modules :global, :local etc)\n    'at-rule-no-unknown': [\n      true,\n      {\n        ignoreAtRules: ['global', 'local'],\n      },\n    ],\n  },\n\n  // File patterns to lint\n  ignoreFiles: ['dist/**/*', 'build/**/*', 'node_modules/**/*'],\n}\n"
  },
  {
    "path": "apps/ui-vanilla/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "apps/ui-vanilla/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "apps/ui-vanilla/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "apps/ui-vanilla/vite.config.ts",
    "content": "import path from 'node:path'\n\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, 'src'),\n    },\n  },\n})\n"
  },
  {
    "path": "architecture/microfrontends/player/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "architecture/microfrontends/player/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## React Compiler\n\nThe React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:\n\n```js\nexport default defineConfig([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n\n      // Remove tseslint.configs.recommended and replace with this\n      tseslint.configs.recommendedTypeChecked,\n      // Alternatively, use this for stricter rules\n      tseslint.configs.strictTypeChecked,\n      // Optionally, add this for stylistic rules\n      tseslint.configs.stylisticTypeChecked,\n\n      // Other configs...\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n\nYou can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:\n\n```js\n// eslint.config.js\nimport reactX from 'eslint-plugin-react-x'\nimport reactDom from 'eslint-plugin-react-dom'\n\nexport default defineConfig([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n      // Enable lint rules for React\n      reactX.configs['recommended-typescript'],\n      // Enable lint rules for React DOM\n      reactDom.configs.recommended,\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n"
  },
  {
    "path": "architecture/microfrontends/player/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport tseslint from 'typescript-eslint'\nimport { defineConfig, globalIgnores } from 'eslint/config'\n\nexport default defineConfig([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs.flat.recommended,\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "architecture/microfrontends/player/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>player</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "architecture/microfrontends/player/package.json",
    "content": "{\n  \"name\": \"player\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\",\n    \"run:preview\": \"pnpm build && pnpm preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19.2.0\",\n    \"react-dom\": \"^19.2.0\",\n    \"single-spa-react\": \"^6.0.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.39.1\",\n    \"@types/node\": \"^24.10.1\",\n    \"@types/react\": \"^19.2.5\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@vitejs/plugin-react\": \"^5.1.1\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"eslint-plugin-react-refresh\": \"^0.4.24\",\n    \"globals\": \"^16.5.0\",\n    \"typescript\": \"~5.9.3\",\n    \"typescript-eslint\": \"^8.46.4\",\n    \"vite\": \"^7.2.4\"\n  }\n}\n"
  },
  {
    "path": "architecture/microfrontends/player/src/App.css",
    "content": "#root {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n  filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  a:nth-of-type(2) .logo {\n    animation: logo-spin infinite 20s linear;\n  }\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n"
  },
  {
    "path": "architecture/microfrontends/player/src/App.tsx",
    "content": "import { useState } from 'react'\nimport './App.css'\n\nfunction App() {\n  const [count, setCount] = useState(0)\n\n  return <>player</>\n}\n\nexport default App\n"
  },
  {
    "path": "architecture/microfrontends/player/src/index.css",
    "content": ":root {\n  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "architecture/microfrontends/player/src/main.tsx",
    "content": "import './index.css'\nimport singleSpaReact from 'single-spa-react'\nimport React from 'react'\nimport ReactDOMClient from 'react-dom/client'\nimport App from './App.tsx'\n\n// createRoot(document.getElementById('root')!).render(\n//   <StrictMode>\n//     <App />\n//   </StrictMode>,\n// )\n\nexport const { bootstrap, mount, unmount } = singleSpaReact({\n  React,\n  ReactDOMClient,\n  domElementGetter: () => document.getElementById('dashboard-root')!,\n  rootComponent: App,\n  errorBoundary(err, info, props) {\n    return <div>This renders when a catastrophic error occurs</div>\n  },\n})\n"
  },
  {
    "path": "architecture/microfrontends/player/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"types\": [\"vite/client\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"erasableSyntaxOnly\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "architecture/microfrontends/player/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "architecture/microfrontends/player/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "architecture/microfrontends/player/vite.config.readme.md",
    "content": "# Детальное объяснение конфигураций Vite для Player Microfrontend\n\nЭтот файл содержит подробное описание всех настроек в `vite.config.ts` для микрофронтенд приложения Player.\n\n---\n\n## 1. **`server.cors: true`** (строка 10)\n\n### Что делает:\n\nВключает Cross-Origin Resource Sharing (CORS) для dev-сервера Vite.\n\n### Зачем нужно:\n\n- Когда root приложение (на порту 6010) пытается загрузить модуль player (с порта 6011), браузер видит это как запрос между разными источниками (origins)\n- По умолчанию браузеры блокируют такие запросы из соображений безопасности\n- `cors: true` добавляет HTTP заголовки, разрешающие кросс-доменные запросы:\n  - `Access-Control-Allow-Origin: *` - разрешает запросы с любого домена\n  - `Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS`\n  - `Access-Control-Allow-Headers: *`\n\n### Пример проблемы без этого:\n\n```\nAccess to fetch at 'http://localhost:6011/index.js' from origin 'http://localhost:6010'\nhas been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present\n```\n\n---\n\n## 2. **`build.lib`** (строки 18-23)\n\n### Что делает:\n\nНастраивает Vite для сборки библиотеки (library mode) вместо обычного веб-приложения.\n\n### 2.1 **`entry: './src/main.tsx'`**\n\n- **Точка входа** для сборки библиотеки\n- Vite начнет сборку с этого файла и включит все его зависимости\n- В обычном режиме Vite использует `index.html` как точку входа\n- В режиме библиотеки используется JavaScript/TypeScript файл\n\n### 2.2 **`name: 'player'`**\n\n- **Имя глобальной переменной** для библиотеки (используется для UMD/IIFE форматов)\n- В нашем случае не критично, так как мы используем ES modules\n- Но если бы использовали UMD формат, библиотека была бы доступна как `window.player`\n\n### 2.3 **`formats: ['es']`**\n\n- **Формат выходного модуля**: ES modules (ECMAScript modules)\n- Альтернативы: `'umd'`, `'cjs'`, `'iife'`\n- ES modules используют `import/export` синтаксис\n- Это современный стандарт, который понимают все современные браузеры\n- Single-SPA может динамически загружать ES модули через `import()`\n\n**Почему ES модули:**\n\n```javascript\n// В root/src/App.tsx:\napp: () => import('http://localhost:6011/index.js')\n// Это работает только если index.js - ES модуль с export\n```\n\n### 2.4 **`fileName: () => 'index.js'`**\n\n- **Имя выходного файла** после сборки\n- Функция позволяет динамически генерировать имя\n- В данном случае всегда возвращает `'index.js'`\n- Без этого Vite создал бы файл типа `player.es.js` или `player.mjs`\n\n**Результат:** После сборки создается `dist/index.js` - один ES модуль\n\n---\n\n## 3. **`rollupOptions.output.inlineDynamicImports: true`** (строка 26)\n\n### Что делает:\n\nОбъединяет все динамические импорты в один файл вместо создания отдельных chunks.\n\n### Без этой опции:\n\nVite/Rollup создал бы несколько файлов:\n\n```\ndist/\n  ├── index.js (основной файл)\n  ├── chunk-abc123.js (код React)\n  ├── chunk-def456.js (код single-spa-react)\n  └── chunk-ghi789.js (другие зависимости)\n```\n\n### С этой опцией:\n\n```\ndist/\n  └── index.js (весь код в одном файле)\n```\n\n### Зачем нужно для микрофронтендов:\n\n- Root приложение загружает только один URL: `http://localhost:6011/index.js`\n- Нет необходимости отслеживать и загружать дополнительные chunk'и\n- Упрощает деплой и управление версиями\n- Single-SPA ожидает один модуль с экспортами `bootstrap`, `mount`, `unmount`\n\n### Недостатки:\n\n- Больший размер файла (326 KB вместо распределенных chunks)\n- Нет возможности кешировать общие зависимости отдельно\n- Для production обычно используют более сложную стратегию с Module Federation\n\n---\n\n## 4. **`define: { 'process.env.NODE_ENV': JSON.stringify('production') }`** (строки 30-32)\n\n### Что делает:\n\nЗаменяет все вхождения `process.env.NODE_ENV` в коде на строку `\"production\"` во время сборки.\n\n### Проблема, которую это решает:\n\nReact и многие библиотеки используют проверки:\n\n```javascript\nif (process.env.NODE_ENV !== 'production') {\n  console.warn('Development warning message')\n  // Дополнительные проверки для разработки\n}\n```\n\n**В Node.js** `process.env.NODE_ENV` - это переменная окружения.\n**В браузере** объекта `process` не существует!\n\n### Без define:\n\n```javascript\n// В собранном index.js будет:\nif (process.env.NODE_ENV !== 'production') { ... }\n// ❌ Ошибка: ReferenceError: process is not defined\n```\n\n### С define:\n\n```javascript\n// Vite заменяет во время сборки:\nif (\"production\" !== 'production') { ... }\n// После минификации это условие полностью удаляется\n```\n\n### Дополнительные эффекты:\n\n1. **Удаление debug кода:** Минификатор видит `if (\"production\" !== 'production')` → всегда false → удаляет весь блок\n2. **Уменьшение размера:** React production сборка на ~30% меньше dev версии\n3. **Производительность:** Убираются все development проверки и warnings\n\n### Альтернативы:\n\n```javascript\ndefine: {\n  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')\n}\n// Использует реальную переменную окружения если есть\n```\n\n---\n\n## Как все работает вместе\n\n### Сценарий работы:\n\n#### 1. Сборка player:\n\n```bash\npnpm build\n```\n\n- Vite читает `src/main.tsx` (entry point)\n- Собирает все зависимости в один файл (inlineDynamicImports)\n- Заменяет `process.env.NODE_ENV` на `\"production\"`\n- Создает ES модуль `dist/index.js` с экспортами `bootstrap`, `mount`, `unmount`\n\n#### 2. Запуск preview сервера:\n\n```bash\npnpm preview\n```\n\n- Запускает сервер на порту 6011\n- Включает CORS для кросс-доменных запросов\n- Отдает собранный `dist/index.js`\n\n#### 3. Root загружает player:\n\n```javascript\n// root/src/App.tsx\nregisterApplication({\n  name: 'appName',\n  app: () => import('http://localhost:6011/index.js'),\n  // ...\n})\n```\n\n- Браузер делает GET запрос на `http://localhost:6011/index.js`\n- CORS заголовки разрешают запрос с localhost:6010\n- Загружается ES модуль\n- Single-SPA вызывает `bootstrap()`, затем `mount()`\n- Player рендерится в `#dashboard-root`\n\n---\n\n## Размер файла: почему 326 KB?\n\n```\ndist/index.js    326.16 kB │ gzip: 74.94 kB\n```\n\n### Что внутри:\n\n- React (~130 KB) - весь runtime React\n- ReactDOM (~130 KB) - код для работы с DOM\n- single-spa-react (~20 KB) - адаптер для интеграции\n- Ваш код App.tsx (~2 KB)\n- Остальное - полифилы и runtime код\n\n### После gzip:\n\n74.94 KB - то, что реально передается по сети\n\n### Оптимизация для production:\n\n- Использовать Module Federation (Webpack) или Native Federation (Vite)\n- Шарить React между микрофронтендами (external)\n- Использовать import maps для общих зависимостей\n\n---\n\n## Полная конфигурация\n\n```typescript\nimport { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    port: 6011,\n    strictPort: true,\n    cors: true, // 👈 Разрешает кросс-доменные запросы в dev mode\n  },\n  preview: {\n    port: 6011,\n    strictPort: true,\n    cors: true, // 👈 Разрешает кросс-доменные запросы в preview mode\n  },\n  build: {\n    lib: {\n      entry: './src/main.tsx', // 👈 Точка входа для библиотеки\n      name: 'player', // 👈 Имя глобальной переменной\n      formats: ['es'], // 👈 Формат: ES modules\n      fileName: () => 'index.js', // 👈 Имя выходного файла\n    },\n    rollupOptions: {\n      output: {\n        inlineDynamicImports: true, // 👈 Все в один файл\n      },\n    },\n  },\n  define: {\n    'process.env.NODE_ENV': JSON.stringify('production'), // 👈 Заменяет process.env\n  },\n})\n```\n\n---\n\n## Важные замечания\n\n### 1. Dev vs Preview mode\n\n- **Dev mode** (`pnpm dev`): Vite не создает физические файлы, модули обрабатываются на лету\n- **Preview mode** (`pnpm preview`): Сервер отдает уже собранные файлы из `dist/`\n- Для микрофронтендов мы используем preview mode, потому что нужен собранный ES модуль\n\n### 2. CORS в production\n\nВ production CORS настраивается на уровне веб-сервера (nginx, CloudFront, etc.):\n\n```nginx\nadd_header 'Access-Control-Allow-Origin' '*';\nadd_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';\n```\n\n### 3. Альтернативы current setup\n\nДля более сложных сценариев рассмотрите:\n\n- **Webpack Module Federation** - шаринг зависимостей между микрофронтендами\n- **Vite Module Federation Plugin** - аналог для Vite\n- **Import Maps** - нативный браузерный способ управления зависимостями\n- **SystemJS** - альтернативная система модулей для Single-SPA\n"
  },
  {
    "path": "architecture/microfrontends/player/vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    port: 6011,\n    strictPort: true,\n    cors: true,\n  },\n  preview: {\n    port: 6011,\n    strictPort: true,\n    cors: true,\n  },\n  build: {\n    lib: {\n      entry: './src/main.tsx',\n      name: 'player',\n      formats: ['es'],\n      fileName: () => 'index.js',\n    },\n    rollupOptions: {\n      // Исключаем React из bundle - он будет загружен через Import Map\n      external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],\n      output: {\n        // Убираем inlineDynamicImports - разрешаем code splitting\n        // inlineDynamicImports: true,  ← УДАЛЕНО\n\n        // Указываем как импортировать external модули\n        paths: {\n          react: 'react',\n          'react-dom': 'react-dom',\n          'react-dom/client': 'react-dom/client',\n          'react/jsx-runtime': 'react/jsx-runtime',\n        },\n      },\n    },\n  },\n  define: {\n    'process.env.NODE_ENV': JSON.stringify('production'),\n  },\n})\n"
  },
  {
    "path": "architecture/microfrontends/root/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "architecture/microfrontends/root/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## React Compiler\n\nThe React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:\n\n```js\nexport default defineConfig([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n\n      // Remove tseslint.configs.recommended and replace with this\n      tseslint.configs.recommendedTypeChecked,\n      // Alternatively, use this for stricter rules\n      tseslint.configs.strictTypeChecked,\n      // Optionally, add this for stylistic rules\n      tseslint.configs.stylisticTypeChecked,\n\n      // Other configs...\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n\nYou can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:\n\n```js\n// eslint.config.js\nimport reactX from 'eslint-plugin-react-x'\nimport reactDom from 'eslint-plugin-react-dom'\n\nexport default defineConfig([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n      // Enable lint rules for React\n      reactX.configs['recommended-typescript'],\n      // Enable lint rules for React DOM\n      reactDom.configs.recommended,\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n"
  },
  {
    "path": "architecture/microfrontends/root/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport tseslint from 'typescript-eslint'\nimport { defineConfig, globalIgnores } from 'eslint/config'\n\nexport default defineConfig([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs.flat.recommended,\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "architecture/microfrontends/root/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>root</title>\n\n    <!-- Import Map для shared dependencies между микрофронтендами -->\n    <script type=\"importmap\">\n      {\n        \"imports\": {\n          \"react\": \"https://esm.sh/react@19.2.0\",\n          \"react-dom\": \"https://esm.sh/react-dom@19.2.0\",\n          \"react-dom/client\": \"https://esm.sh/react-dom@19.2.0/client\",\n          \"react/jsx-runtime\": \"https://esm.sh/react@19.2.0/jsx-runtime\",\n          \"react/jsx-dev-runtime\": \"https://esm.sh/react@19.2.0/jsx-dev-runtime\"\n        }\n      }\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "architecture/microfrontends/root/package.json",
    "content": "{\n  \"name\": \"root\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19.2.0\",\n    \"react-dom\": \"^19.2.0\",\n    \"single-spa\": \"^6.0.3\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.39.1\",\n    \"@types/node\": \"^24.10.1\",\n    \"@types/react\": \"^19.2.5\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@vitejs/plugin-react\": \"^5.1.1\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"eslint-plugin-react-refresh\": \"^0.4.24\",\n    \"globals\": \"^16.5.0\",\n    \"typescript\": \"~5.9.3\",\n    \"typescript-eslint\": \"^8.46.4\",\n    \"vite\": \"^7.2.4\"\n  }\n}\n"
  },
  {
    "path": "architecture/microfrontends/root/src/App.css",
    "content": "#root {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n  filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  a:nth-of-type(2) .logo {\n    animation: logo-spin infinite 20s linear;\n  }\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n"
  },
  {
    "path": "architecture/microfrontends/root/src/App.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport './App.css'\nimport { registerApplication, start } from 'single-spa'\n\nfunction App() {\n  const [count, setCount] = useState(0)\n  const [menuItems, setMenuItems] = useState([])\n\n  useEffect(() => {\n    registerApplication({\n      name: 'appName',\n      app: () => import('http://localhost:6011/index.js'),\n      activeWhen: '',\n      customProps: {\n        authToken: 'xc67f6as87f7s9d',\n        action: setCount,\n        value: count,\n        setMenuItems: (newItems) => setMenuItems((prev) => [...prev, ...newItems]),\n      },\n    })\n    start()\n  }, [])\n\n  return (\n    <>\n      I am a host {count}\n      <ul>\n        <li>host menu 1</li>\n        <li>host menu 2</li>\n        {menuItems.map((item, index) => (\n          <li key={index}>{item.title}</li>\n        ))}\n      </ul>\n      <div id={'dashboard-root'}></div>\n    </>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "architecture/microfrontends/root/src/index.css",
    "content": ":root {\n  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "architecture/microfrontends/root/src/main.tsx",
    "content": "import { createRoot } from 'react-dom/client'\nimport './index.css'\nimport App from './App.tsx'\n\ncreateRoot(document.getElementById('root')!).render(<App />)\n"
  },
  {
    "path": "architecture/microfrontends/root/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"types\": [\"vite/client\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"erasableSyntaxOnly\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "architecture/microfrontends/root/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "architecture/microfrontends/root/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "architecture/microfrontends/root/vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    port: 6010,\n    strictPort: true,\n  },\n  preview: {\n    port: 6010,\n    strictPort: true,\n  },\n})\n"
  },
  {
    "path": "content-thoughts/search-input/info.md",
    "content": "https://chatgpt.com/c/683de0c6-bd6c-8006-9b72-b97880e5e855\n"
  },
  {
    "path": "docs/feature-comparison.md",
    "content": "# MusicFun: Feature Comparison (RTK Query vs TanStack Query + Zustand)\n\nПроверка кода: **2026-02-24**  \nСравнивались:\n\n- `apps/rtk-query`\n- `apps/tanstack-query-zustand`\n\nЦель документа: зафиксировать **актуальный** функциональный паритет и реальные расхождения по коду.\n\n---\n\n## 1. Краткий статус\n\n| Блок                                    | Статус | Комментарий                                                                          |\n| --------------------------------------- | ------ | ------------------------------------------------------------------------------------ |\n| MainPage                                | ✅     | Базовый сценарий данных/карточек/playback синхронизирован                            |\n| TracksPage                              | ✅     | Поиск/сортировка/playback/очередь синхронизированы                                   |\n| TrackPage                               | ✅     | Skeleton, gradient, playback/control panel синхронизированы                          |\n| PlaylistPage                            | ✅     | Skeleton, gradient, reactions, edit, fallback `duration: 100` синхронизированы       |\n| UserPage core (profile/tabs/background) | ✅     | URL-tab sync, background extraction по avatar, skeleton логика есть                  |\n| Header + account menu                   | ✅     | Поведение account menu и logout flow в рабочем паритете                              |\n| Routing                                 | ⚠️     | Есть различие в path-шаблоне lyrics-route (см. ниже)                                 |\n| UserPage: Liked Playlists tab           | ⚠️     | Контент/поведение отличается от RTK (см. ниже)                                       |\n| Pagination behavior                     | ⚠️     | В TanStack `Pagination` теперь всегда видима, в RTK скрывается при `pagesCount <= 1` |\n\n---\n\n## 2. Что синхронизировано (актуально)\n\n### 2.1 UserPage background color\n\n- В TanStack `useUserPageBackgroundColor` использует `profile avatar -> decode base64 -> dominant color`, как RTK.\n- Старый gap по этому блоку закрыт.\n\n### 2.2 UserPage tabs + URL\n\n- В TanStack таб берется из `?tab=...`, как в RTK.\n- Вкладки используют `searchParams` для `page` и pagination state.\n\n### 2.3 PlaylistPage duration\n\n- В обоих проектах в таблице треков плейлиста используется временный fallback `duration: 100` до фикса API.\n\n### 2.4 Edit profile flow\n\n- В обоих проектах есть modal flow редактирования профиля, сохранение в `localStorage`-профиль, обновление avatar/fullName в состоянии приложения.\n- В TanStack есть crop-step при загрузке изображения.\n\n### 2.5 Header / account menu\n\n- В обоих проектах:\n  - skeleton в auth action-зоне при `me` loading;\n  - отображение avatar/fullName/login;\n  - переход в профиль;\n  - logout.\n\n---\n\n## 3. Актуальные расхождения\n\n### 3.1 Routing: lyrics path differs\n\n- **RTK:** маршрут трека с лирикой — `/tracks/lyrics/:id` (через `Paths.TracksLyrics`).\n- **TanStack:** маршрут — `/tracks/:id/lyrics`.\n\nОба приложения работоспособны в пределах своего роутинга, но 1:1 parity по URL-формату отсутствует.\n\n### 3.2 MyLikedPlaylistsTab: поведение отличается от RTK\n\n- **RTK:** карточки во вкладке включают owner actions (edit/delete dropdown).\n- **TanStack:** вкладка приведена к карточке main-page типа (reactions + owner/date/tracks), но без owner edit/delete actions.\n\nЭто осознанное UX-расхождение относительно RTK-референса.\n\n### 3.3 Pagination component behavior\n\n- **RTK `Pagination`:** `return null` при `pagesCount <= 1`.\n- **TanStack `Pagination`:** после текущих правок компонент рендерится всегда (нормализация до минимум 1 страницы).\n\nЭто глобальное различие поведения компонента, не только в UserPage табах.\n\n### 3.4 Tab switch page-reset behavior\n\n- **TanStack `UserTabs`:** при переключении таба удаляется `page` из query.\n- **RTK `UserTabs`:** `page` не сбрасывается при смене таба.\n\nПоведение отличается, хотя оба варианта функционально корректны.\n\n### 3.5 PlaylistsPage token gate implementation\n\n- В TanStack в `PlaylistsPage` применена project-specific проверка наличия токенов через raw localStorage keys.\n- В RTK такой логики gate нет в этом месте.\n\nЭто проектное техническое отличие, а не критический UX-breaker.\n\n---\n\n## 4. Вывод\n\nНа уровне пользовательских сценариев проекты сейчас близки к высокому паритету.  \nОсновные несоответствия локализованы в:\n\n1. формате lyrics-route URL;\n2. содержимом/действиях в `MyLikedPlaylistsTab`;\n3. глобальном поведении компонента `Pagination`;\n4. деталях query-param поведения при переключении табов;\n5. проектно-специфичной token-gate логике в TanStack `PlaylistsPage`.\n"
  },
  {
    "path": "docs/todos-features.md",
    "content": "# TODO: Features\n\n## Актуальные статусы (проверка кода от 2026-02-23)\n\nНиже — фактический статус задач в `tanstack-query-zustand` относительно RTK-референса.\n\n## Закрытые задачи последнего цикла\n\n- [x] **UserPage: background color extraction как в RTK**\n\n  - ✅ В `useUserPageBackgroundColor` подключён avatar из profile store + `decodeFileFromBase64`.\n  - ✅ Логика вычисления dominant color приведена к RTK-подходу (для owner-профиля).\n\n- [x] **UserPage tabs: pagination через URL как в RTK**\n\n  - ✅ `PlaylistsTab`, `TracksTab`, `LikedTracksTab`, `MyLikedPlaylistsTab` переведены с `useState(pageNumber)` на `searchParams(page)`.\n  - ✅ Восстановление состояния страницы после reload/back-forward теперь работает через URL.\n\n- [x] **UserPage > MyLikedPlaylistsTab: owner actions parity**\n\n  - ✅ Для карточек в табе включены owner actions (edit/delete) через `canEdit`.\n\n- [x] **PlaylistPage tracks: унифицирован fallback для duration**\n\n  - ✅ До исправления API выставлен единый с RTK fallback (`duration: 100`).\n\n- [x] **PlaylistsPage (tanstack): project-specific token gate**\n  - ✅ Проверка наличия токенов оставлена через raw `localStorage` ключи (`musicfun-access-token`, `musicfun-refresh-token`).\n  - ✅ Это осознанная проектная реализация для `tanstack-query-zustand` (гейт для initial me-запроса, а не token lifecycle API).\n\n### Критично: паритет поведения\n\n- [x] **TracksPage (tanstack): воспроизведение и очередь как в RTK**\n\n  - ✅ Добавлен toggle play/pause для текущего трека.\n  - ✅ Добавлен запуск с `playlistId = all-tracks` и полной очередью.\n  - ✅ Добавлено дописывание новых треков в queue при infinite scroll, если активен `all-tracks`.\n\n- [x] **UserPage > TracksTab / LikedTracksTab (tanstack): playback**\n  - ✅ Подключён реальный `onPlayClick` в обоих табах.\n  - ✅ Добавлены play/pause/resume для текущего трека.\n  - ✅ Добавлена загрузка/использование playlist queue для табов.\n\n### Паритет UI/flows\n\n- [x] **TrackActions (tanstack): Edit track**\n\n  - ✅ Реализован prefill в modal по `editingTrackId`.\n  - ✅ Реализован `PUT /playlists/tracks/{trackId}` при сохранении.\n\n- [x] **TrackPage (tanstack): кнопка Play в ControlPanel**\n\n  - ✅ Подключён обработчик Play/Pause для текущего трека.\n\n- [x] **PlaylistPage (tanstack): Edit playlist из ControlPanel**\n\n  - ✅ Реализован prefill в modal по `editingPlaylistId`.\n  - ✅ Реализован `PUT /playlists/{playlistId}` при сохранении.\n\n- [x] **PlaylistCard (tanstack): Edit в карточке**\n\n  - ✅ Использует тот же рабочий edit-flow через `editingPlaylistId` + update mutation.\n\n- [x] **UserPage (tanstack): редактирование профиля (как в RTK)**\n\n  - ✅ Подключён `EditProfileModal` через `Layout`.\n  - ✅ Подключена гидрация profile state из `localStorage` по текущему пользователю.\n  - ✅ Кнопка `Edit profile` в `UserInfo` открывает модалку и сохраняет изменения в `profile-store`.\n\n- [x] **Header (tanstack): поведение AccountMenu как в RTK**\n\n  - ✅ В хедере добавлен skeleton для auth-action блока во время `me` loading.\n  - ✅ `ProfileDropdownMenu` использует `avatar/fullName/login` и fallback-логику имени как в RTK.\n  - ✅ На logout выполняется очистка `profile-store`.\n\n- [x] **UserPage tabs (tanstack): активный таб сохраняется в URL**\n  - ✅ Текущий таб берётся из `?tab=...`.\n  - ✅ При переключении таба URL обновляется.\n  - ✅ После reload восстанавливается актуальный таб.\n\n### UX / данные / скелетоны (частичный паритет)\n\n- [x] **UserPage (tanstack): реальный avatar вместо hardcoded**\n\n  - ✅ Убран hardcoded `unsplash` в `UserInfo` и `Header`.\n  - ✅ Добавлен общий `Avatar` компонент с fallback инициалов (как в RTK-подходе).\n  - ℹ️ API `/auth/me` не возвращает avatar URL, поэтому используется корректный fallback без фиктивной картинки.\n\n- [x] **UserPage (tanstack): skeleton в табах при initial loading**\n\n  - ✅ Вместо `null` добавлен `UserTabsSkeleton`.\n\n- [x] **TrackPage / PlaylistPage (tanstack): skeleton parity**\n\n  - ✅ Добавлены `TrackPageSkeleton` и `PlaylistPageSkeleton`.\n\n- [x] **TrackPage / PlaylistPage (tanstack): background color extraction parity**\n\n  - ✅ Подключён `usePageBackgroundColor` на обеих страницах (`canvasRef` + `backgroundColor`).\n\n- [x] **PlaylistPage (tanstack): поиск по трекам в таблице**\n\n  - ✅ Добавлен `SearchTextField` и фильтрация треков по названию.\n  - ✅ При поиске queue/play-all работают от отфильтрованного списка.\n\n- [x] **TracksPage (tanstack): синхронизация selected sort с URL**\n\n  - ✅ Значение `SortSelect` вычисляется из `sortBy/sortDirection` из URL.\n\n- [x] **UserPage tracks data (tanstack): заглушки убраны**\n  - ✅ Значения `duration`/`dislikesCount` читаются из API с fallback.\n  - ✅ Жёстко прописанные mock-значения в `UserPage` табах устранены.\n"
  },
  {
    "path": "eslint.config.js",
    "content": "// Root ESLint configuration for monorepo\n// Each app can have its own eslint.config.js for app-specific rules\n\nimport js from '@eslint/js'\nimport prettier from 'eslint-config-prettier'\nimport eslintPluginPrettier from 'eslint-plugin-prettier'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport simpleImportSort from 'eslint-plugin-simple-import-sort'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default [\n  // Global ignores\n  {\n    ignores: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/build/**',\n      '**/.next/**',\n      '**/coverage/**',\n      '**/.vite/**',\n    ],\n  },\n\n  // Base configuration for all JS/TS files\n  {\n    files: ['**/*.{js,jsx,ts,tsx,mjs,cjs}'],\n    extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: {\n        ...globals.browser,\n        ...globals.node,\n        ...globals.es2020,\n      },\n    },\n    plugins: {\n      'react-hooks': reactHooks,\n      'react-refresh': reactRefresh,\n      prettier: eslintPluginPrettier,\n      'simple-import-sort': simpleImportSort,\n    },\n    rules: {\n      ...reactHooks.configs.recommended.rules,\n      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],\n      'prettier/prettier': 'warn',\n      'simple-import-sort/imports': 'error',\n      'simple-import-sort/exports': 'error',\n      '@typescript-eslint/no-unused-vars': [\n        'warn',\n        {\n          argsIgnorePattern: '^_',\n          varsIgnorePattern: '^_',\n        },\n      ],\n    },\n  },\n]\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\nrouteTree.gen.ts\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/README.md",
    "content": "https://chatgpt.com/c/6867c378-f864-8006-85a1-98da24df147c?model=o4-mini-high\n\n## Create Vite React Project\n\npnpm create vite\nhttps://vite.dev/guide/\n\n## Tanstack Installation\n\nhttps://tanstack.com/query/latest/docs/framework/react/installation\n\npnpm add @tanstack/react-query\n\n## clear app.tsx\n\n```tsx\nfunction App() {\n  return <>Hello</>\n}\n\nexport default App\n```\n\n## remove StrictMode\n\nin main.tsx\n\n## activate eslint on save\n\n## run project\n\npnpm dev\n\n## generate api layer\n\nhttps://openapi-ts.dev/introduction\n\npnpm i -D openapi-typescript typescript\n\nAnd in your tsconfig.json, to load the types properly:\n\"compilerOptions\": {\n\"module\": \"ESNext\", // or \"NodeNext\"\n\"moduleResolution\": \"Bundler\" // or \"NodeNext\"\n}\n\nHighly recommended\n\nAlso adding the following can boost type safety:\n\ntsconfig.json\n\n{\n\"compilerOptions\": {\n\"noUncheckedIndexedAccess\": true\n}\n}\n\n## install openapi-fetch\n\npnpm i openapi-fetch\n\n## add script and generate api\n\n\"generate:api\": \"pnpm dlx openapi-typescript https://spotifun.it-incubator.app/api-json -o ./src/shared/api/schema.ts\"\n\n# add env files\n\n.env\nVITE_BASE_URL=https://spotifun.it-incubator.app/api/1.0\nVITE_API_KEY=\nVITE_CURRENT_DOMAIN=http://localhost:5174\n\n.env.local\nVITE_API_KEY=72c3121c-c679-4c0e-9131-2d3f35e6a3bd\n\n## add client.tsx\n\n```typescript\nimport createClient, { type Middleware } from 'openapi-fetch'\nimport type { paths } from './schema.ts'\n\nconst config = {\n  baseURL: null as string | null,\n  apiKey: null as string | null,\n  getAccessToken: null as (() => Promise<string | null>) | null,\n  saveAccessToken: null as (() => Promise<void>) | null,\n  getRefreshToken: null as (() => Promise<string | null>) | null,\n  saveRefreshToken: null as (() => Promise<void>) | null,\n}\n\nexport const setClientConfig = (newConfig: Partial<typeof config>) => {\n  Object.assign(config, newConfig)\n  _client = undefined // пере-инициализируем\n}\n\nconst authMiddleware: Middleware = {\n  /* ---------- REQUEST -------------------------------------------------- */\n  async onRequest({ request }) {\n    request.headers.set('API-KEY', config.apiKey!)\n\n    const token = await config.getAccessToken?.()\n    if (token) request.headers.set('Authorization', `Bearer ${token}`)\n\n    return request\n  },\n}\n\nlet _client: ReturnType<typeof createClient<paths>> | undefined\n\nexport const getClient = () => {\n  if (_client) return _client\n\n  if (!config.baseURL || !config.apiKey) {\n    console.error('call setClientConfig to setup api')\n    throw new Error('call setClientConfig to setup api')\n  }\n\n  const client = createClient<paths>({ baseUrl: config.baseURL })\n  client.use(authMiddleware)\n  _client = client\n  return _client\n}\n```\n\n## setup client in main.tsx\n\n```tsx\n\n```\n\n## const queryClient = new QueryClient()\n\n```tsx\nconst queryClient = new QueryClient()\n\nfunction App() {\n  return (\n    // Provide the client to your App\n    <QueryClientProvider client={queryClient}>\n      <Playlists />\n    </QueryClientProvider>\n  )\n}\n```\n\n## add page Playlists\n\nsrc/pages/playlists/playlists.tsx\n\n```tsx\n\n```\n\n## devtool\n\npnpm i @tanstack/react-query-devtools\n\n## install tanstack router\n\npnpm add @tanstack/react-router\npnpm install -D @tanstack/router-plugin\n\nhttps://tanstack.com/router/latest/docs/framework/react/quick-start\n\n```typescript\ntanstackRouter({\n  target: 'react',\n  autoCodeSplitting: true,\n})\n```\n\n1. при добавляении 2 страницы.. обратить внимание.. что нет рефетчей..\n\nhttps://miro.com/app/board/uXjVIhdj_Vw=/?share_link_id=990510541124\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport pluginQuery from '@tanstack/eslint-plugin-query'\nimport { globalIgnores } from 'eslint/config'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config([\n  ...pluginQuery.configs['flat/recommended'],\n  globalIgnores(['dist']),\n  {\n    plugins: {\n      '@tanstack/query': pluginQuery,\n    },\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs['recommended-latest'],\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite + React + TS</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/app/entrypoint/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/orval.config.cjs",
    "content": "module.exports = {\n  musicfun: {\n    input: { target: 'https://musicfun.it-incubator.app/api-json' },\n    output: {\n      target: './src/shared/api/orval/musicfun.ts',\n      mode: 'tags-split',\n      client: 'react-query',\n      // baseUrl: {\n      //   getBaseUrlFromSpecification: true,\n      //   variables: {\n      //     environment: 'api.dev',\n      //   },\n      // },\n      override: {\n        mutator: {\n          path: './src/shared/api/orval/custom-instance.ts',\n          name: 'customInstance',\n        },\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/package.json",
    "content": "{\n  \"name\": \"tanstack-query-musicfun-small-example\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\",\n    \"generate:api\": \"pnpm dlx openapi-typescript https://musicfun.it-incubator.app/api-json --root-types -o ./src/shared/api/schema.ts\",\n    \"generate:async-api\": \"pnpm asyncapi generate models typescript https://musicfun.it-incubator.app/async-api-json  -o ./src/shared/async-api/schema.ts --tsEnumType union --tsModelType interface\",\n    \"generate:async-api:dimych\": \"pnpm asyncapi generate models typescript http://localhost:9001/async-api-json -o ./src/shared/async-api --tsModelType interface --tsEnumType union --tsRawPropertyNames\",\n    \"generate:api:dimych\": \"pnpm dlx openapi-typescript http://localhost:9001/api-json -o ./src/shared/api/schema.ts\",\n    \"generate:orval\": \" orval --input https://musicfun.it-incubator.app/api-json --output ./src/shared/api/orval/musicfun.ts\",\n    \"__generate:orval\": \"orval --config orval.config.cjs\"\n  },\n  \"dependencies\": {\n    \"@tanstack/react-query\": \"^5.81.5\",\n    \"@tanstack/react-query-devtools\": \"^5.81.5\",\n    \"@tanstack/react-router\": \"^1.124.0\",\n    \"axios\": \"^1.12.2\",\n    \"openapi-fetch\": \"^0.14.0\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"react-hook-form\": \"^7.60.0\",\n    \"react-toastify\": \"11.0.5\",\n    \"socket.io-client\": \"4.8.1\"\n  },\n  \"devDependencies\": {\n    \"@asyncapi/cli\": \"^3.2.0\",\n    \"@eslint/js\": \"^9.29.0\",\n    \"@tanstack/eslint-plugin-query\": \"^5.81.2\",\n    \"@tanstack/router-plugin\": \"^1.124.0\",\n    \"@types/react\": \"^19.1.8\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@vitejs/plugin-react\": \"^4.5.2\",\n    \"eslint\": \"^9.29.0\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.20\",\n    \"globals\": \"^16.2.0\",\n    \"openapi-typescript\": \"^7.8.0\",\n    \"orval\": \"^7.11.2\",\n    \"path\": \"^0.12.7\",\n    \"typescript\": \"~5.8.3\",\n    \"typescript-eslint\": \"^8.34.1\",\n    \"vite\": \"^7.0.0\"\n  },\n  \"packageManager\": \"pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac\"\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/entrypoint/main.tsx",
    "content": "import '../styles/reset.css'\nimport '../styles/index.css'\n\nimport { createRouter, RouterProvider } from '@tanstack/react-router'\nimport { createRoot } from 'react-dom/client'\n\nimport { routeTree } from '@/app/routes/routeTree.gen.ts'\nimport { setClientConfig } from '@/shared/api/client.ts'\nimport { apiBaseUrl, apiKey } from '@/shared/config/api.config.ts'\nimport { localStorageKeys } from '@/shared/db/localstorage-keys.ts'\n\nconst router = createRouter({ routeTree })\n\n// Register the router instance for type safety\ndeclare module '@tanstack/react-router' {\n  interface Register {\n    router: typeof router\n  }\n}\n\nsetClientConfig({\n  baseURL: apiBaseUrl,\n  apiKey: apiKey,\n  getAccessToken: async () => localStorage.getItem(localStorageKeys.accessToken),\n  getRefreshToken: async () => localStorage.getItem(localStorageKeys.refreshToken),\n  saveAccessToken: async (token) =>\n    token\n      ? localStorage.setItem(localStorageKeys.accessToken, token)\n      : localStorage.removeItem(localStorageKeys.accessToken),\n  saveRefreshToken: async (token) =>\n    token\n      ? localStorage.setItem(localStorageKeys.refreshToken, token)\n      : localStorage.removeItem(localStorageKeys.refreshToken),\n})\n\ncreateRoot(document.getElementById('root')!).render(<RouterProvider router={router} />)\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/layouts/root-layout.module.css",
    "content": ".container {\n  padding-top: 10px;\n  max-width: 900px;\n  margin: 0 auto;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/layouts/root-layout.tsx",
    "content": "import { QueryClientProvider } from '@tanstack/react-query'\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools'\nimport { Outlet } from '@tanstack/react-router'\nimport { ToastContainer } from 'react-toastify'\n\nimport styles from '@/app/layouts/root-layout.module.css'\nimport { WebSocketProvider } from '@/app/providers/web-socket-provider.tsx'\nimport { queryClient } from '@/app/query-client/query-client.tsx'\nimport { AccountBar } from '@/features/auth'\nimport { Header } from '@/shared/ui/header/header.component.tsx'\n\nexport function RootLayout() {\n  return (\n    <>\n      <QueryClientProvider client={queryClient}>\n        <WebSocketProvider>\n          <Header renderAccountBar={() => <AccountBar />} />\n          <div className={styles.container}>\n            <Outlet />\n          </div>\n          <ReactQueryDevtools initialIsOpen={false} buttonPosition={'bottom-left'} />\n          <ToastContainer />\n        </WebSocketProvider>\n      </QueryClientProvider>\n    </>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/providers/web-socket-provider.tsx",
    "content": "import type { ReactNode } from 'react'\n\nexport function WebSocketProvider({ children }: { children: ReactNode }) {\n  return <>{children}</>\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/query-client/query-client.tsx",
    "content": "import { MutationCache, QueryClient } from '@tanstack/react-query'\n\nimport { mutationGlobalErrorHandler } from '@/shared/api/query-error-handler-for-rhf-factory.ts'\n\nexport type MutationMeta = {\n  /**\n   * Если 'off' — глобальный обработчик ошибок пропускаем,\n   * если 'on' (или нет поля) — вызываем.\n   */\n  globalErrorHandler?: 'on' | 'off'\n}\n\ndeclare module '@tanstack/react-query' {\n  interface Register {\n    /**\n     * Тип для поля `meta` в useMutation(...)\n     */\n    mutationMeta: MutationMeta\n  }\n}\n\nexport const queryClient = new QueryClient({\n  mutationCache: new MutationCache({\n    onError: mutationGlobalErrorHandler, // 🔹 вызывается ВСЕГДА\n  }),\n  defaultOptions: {\n    queries: {\n      refetchOnWindowFocus: false,\n      refetchOnMount: false,\n      staleTime: Infinity, //5000,\n      //gcTime: 10000 // если нет подписчиков - удалить всё нафик...\n    },\n  },\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/routes/__root.tsx",
    "content": "import 'react-toastify/dist/ReactToastify.css'\n\nimport { createRootRoute } from '@tanstack/react-router'\n\nimport { RootLayout } from '@/app/layouts/root-layout.tsx'\n\nexport const Route = createRootRoute({\n  component: RootLayout,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/routes/index.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { Playlists } from '../../features/playlists/list/playlists.tsx'\n\nexport const Route = createFileRoute('/')({\n  component: () => <Playlists filtersEnabled={false} />,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/routes/my-playlists.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { MyPlaylistsPage } from '@/pages/playlists/ui/my-playlists/my-playlists-page.tsx'\nimport { ROUTES } from '@/shared/routes/routes.ts'\n\nexport const Route = createFileRoute(ROUTES.myPlaylists)({\n  component: MyPlaylistsPage,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/routes/oauth/callback.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { OauthCallbackPage } from '@/pages/auth'\n\nexport const Route = createFileRoute('/oauth/callback')({\n  component: OauthCallbackPage,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/routes/playlists-with-filters.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { PlaylistsWithFiltersPage } from '@/pages/playlists/ui/playlists-with-filters-page.tsx'\n\nexport const Route = createFileRoute('/playlists-with-filters')({\n  component: PlaylistsWithFiltersPage,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/styles/index.css",
    "content": "body {\n  font-family:\n    system-ui,\n    -apple-system,\n    BlinkMacSystemFont,\n    'Segoe UI',\n    Roboto,\n    'Helvetica Neue',\n    Arial,\n    sans-serif;\n  background: #060707;\n\n  color: #9c9c9c;\n}\na {\n  color: #9c9c9c;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/app/styles/reset.css",
    "content": "/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Prevent font size inflation */\nhtml {\n  -moz-text-size-adjust: none;\n  -webkit-text-size-adjust: none;\n  text-size-adjust: none;\n}\n\n/* Remove default margin in favour of better control in authored CSS */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nfigure,\nblockquote,\ndl,\ndd {\n  margin-block-end: 0;\n}\n\n/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */\nul,\nol {\n  list-style: none;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  line-height: 1.5;\n}\n\n/* Set shorter line heights on headings and interactive elements */\nh1,\nh2,\nh3,\nh4,\nbutton,\ninput,\nlabel {\n  line-height: 1.1;\n}\n\n/* Balance text wrapping on headings */\nh1,\nh2,\nh3,\nh4 {\n  text-wrap: balance;\n}\n\n/* A elements that don't have a class get default styles */\na:not([class]) {\n  text-decoration-skip-ink: auto;\n  color: currentColor;\n}\n\n/* Make images easier to work with */\nimg,\npicture {\n  max-width: 100%;\n  display: block;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n/* Make sure textareas without a rows attribute are not tiny */\ntextarea:not([rows]) {\n  min-height: 10em;\n}\n\n/* Anything that has been anchored to should have extra scroll margin */\n:target {\n  scroll-margin-block: 5ex;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/api/auth-api.types.ts",
    "content": "import { getClientConfig } from '@/shared/api/client.ts'\nimport type { SchemaLoginRequestPayload } from '@/shared/api/schema.ts'\n\nexport type LoginRequestPayload = SchemaLoginRequestPayload\n\nexport const getOauthRedirectUrl = (redirectUrl: string) =>\n  getClientConfig().baseURL + `/auth/oauth-redirect?callbackUrl=${redirectUrl}`\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/api/use-login.mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '../../../shared/api/client.ts'\nimport { localStorageKeys } from '../../../shared/db/localstorage-keys.ts'\nimport type { LoginRequestPayload } from './auth-api.types.ts'\n\nexport const useLoginMutation = () => {\n  const qc = useQueryClient()\n  return useMutation({\n    mutationFn: (payload: LoginRequestPayload) => {\n      return getClient().POST('/auth/login', {\n        body: payload,\n      })\n    },\n    onSuccess: async (data) => {\n      localStorage.setItem(localStorageKeys.refreshToken, data.data!.refreshToken)\n      localStorage.setItem(localStorageKeys.accessToken, data.data!.accessToken)\n      await qc.invalidateQueries({ queryKey: ['auth'] })\n\n      await qc.invalidateQueries()\n    },\n  })\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/api/use-logout.mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '../../../shared/api/client.ts'\nimport { localStorageKeys } from '../../../shared/db/localstorage-keys.ts'\n\nexport const useLogoutMutation = () => {\n  const qc = useQueryClient()\n  return useMutation({\n    mutationFn: () => {\n      return getClient().POST('/auth/logout', {\n        body: {\n          refreshToken: localStorage.getItem(localStorageKeys.refreshToken)!,\n        },\n      })\n    },\n    onSuccess: async () => {\n      localStorage.removeItem(localStorageKeys.accessToken)\n      localStorage.removeItem(localStorageKeys.refreshToken)\n      qc.resetQueries({ queryKey: ['auth'] }) // resetQueries переводит query в изначальное состояние и уведомляет подписчиков — компонент получит data = undefined.\n      //qc.invalidateQueries({ queryKey: [authKey] }) // invalidateQueries заставит его немедленно перефетчиться без токена ⇒ получите 401 ⇒ data станет undefined / error.\n    },\n  })\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/api/use-me.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\nimport { requestWrapper } from '@/shared/api/request-wrapper.ts'\n\nexport const useMeQuery = () => {\n  return useQuery({\n    queryKey: ['auth', 'me'],\n    queryFn: () => requestWrapper(getClient().GET('/auth/me')),\n    retry: false,\n  })\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/index.tsx",
    "content": "export { AccountBar } from './ui/account-bar.tsx'\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/ui/account-bar.module.css",
    "content": ".meInfoContainer {\n  display: flex;\n  gap: 10px;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/ui/account-bar.tsx",
    "content": "import { CurrentUser } from '@/features/auth/ui/current-user/current-user.tsx'\nimport { LoginButton } from '@/features/auth/ui/login-button/login-button.tsx'\n\nimport { useMeQuery } from '../api/use-me.query.ts'\n\nexport const AccountBar = () => {\n  const query = useMeQuery()\n\n  return (\n    <div>\n      {!query.data && <LoginButton />}\n      {query.data && <CurrentUser />}\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/ui/current-user/current-user.tsx",
    "content": "import { Link } from '@tanstack/react-router'\n\nimport { LogoutButton } from '@/features/auth/ui/logout-button/logout-button.tsx'\n\nimport { useMeQuery } from '../../api/use-me.query.ts'\nimport styles from '../account-bar.module.css'\n\nexport const CurrentUser = () => {\n  const query = useMeQuery()\n\n  return (\n    <div className={styles.meInfoContainer}>\n      <Link to=\"/my-playlists\" activeOptions={{ exact: true }}>\n        {query.data!.login}\n      </Link>\n\n      <LogoutButton />\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/ui/login-button/login-button.tsx",
    "content": "import { useLogin } from '@/features/auth/ui/login-button/use-login.tsx'\n\nexport const LoginButton = () => {\n  const { login: handleLoginClick } = useLogin()\n\n  return <button onClick={handleLoginClick}>Login with APIHUB</button>\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/ui/login-button/use-login.tsx",
    "content": "import { getOauthRedirectUrl } from '../../api/auth-api.types.ts'\nimport { useLoginMutation } from '../../api/use-login.mutation.ts'\n\nconst currentDomain = import.meta.env.VITE_CURRENT_DOMAIN\n\nexport const useLogin = () => {\n  const { mutate } = useLoginMutation()\n\n  function login() {\n    const redirectUri = currentDomain + '/oauth/callback' // todo: to config\n    const url = getOauthRedirectUrl(redirectUri)\n    window.open(url, 'oauthPopup', 'width=500,height=600')\n\n    const handleOauthMessage = async (event: MessageEvent) => {\n      if (event.origin !== currentDomain) {\n        return\n      }\n\n      const { code } = event.data\n      if (code) {\n        console.log('✅ code received:', code)\n        window.removeEventListener('message', handleOauthMessage)\n        mutate({ code, accessTokenTTL: '10s', redirectUri, rememberMe: true })\n      }\n    }\n\n    window.addEventListener('message', handleOauthMessage)\n  }\n\n  return { login }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/ui/logout-button/logout-button.tsx",
    "content": "import { useLogout } from '@/features/auth/ui/logout-button/use-logout.ts'\n\nexport const LogoutButton = () => {\n  const { logout: handleLogoutClick } = useLogout()\n\n  return <button onClick={handleLogoutClick}>Logout</button>\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/auth/ui/logout-button/use-logout.ts",
    "content": "import { useLogoutMutation } from '@/features/auth/api/use-logout.mutation.ts'\n\nexport const useLogout = () => {\n  const { mutate } = useLogoutMutation()\n\n  const logout = () => {\n    mutate()\n  }\n\n  return { logout }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/add-playlist-form/add-playlist-form.module.css",
    "content": ".form {\n  border-bottom: 1px solid grey;\n  padding: 10px;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/add-playlist-form/add-playlist-form.tsx",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { type SubmitHandler, useForm } from 'react-hook-form'\n\nimport type { components } from '@/shared/api'\nimport { getClient } from '@/shared/api'\nimport { requestWrapper } from '@/shared/api/request-wrapper.ts'\n\nimport styles from './add-playlist-form.module.css'\n\nexport type CreatePlaylistRequestPayload = components['schemas']['CreatePlaylistRequestPayload']\n\nexport const AddPlaylistForm = () => {\n  const queryClient = useQueryClient()\n\n  const {\n    register,\n    handleSubmit,\n    reset,\n    formState: { isSubmitting },\n  } = useForm<CreatePlaylistRequestPayload>({\n    defaultValues: { title: '', description: '' },\n  })\n\n  const { mutate } = useMutation({\n    mutationFn: (body: CreatePlaylistRequestPayload) =>\n      requestWrapper(getClient().POST('/playlists', { body })),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['playlists'] })\n      reset()\n    },\n    onError: (err: unknown) => {\n      console.error(err)\n      alert(JSON.stringify(err))\n      throw err\n    },\n    meta: { globalErrorHandler: 'on' },\n  })\n\n  const onSubmit: SubmitHandler<CreatePlaylistRequestPayload> = (data) => {\n    mutate(data)\n  }\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)} className={styles.form}>\n      <h2>Add a New Playlist</h2>\n\n      <p>\n        <input\n          {...register('title', { required: true })}\n          placeholder=\"Title\"\n          disabled={isSubmitting}\n        />\n      </p>\n\n      <div>\n        <input {...register('description')} placeholder=\"Description\" disabled={isSubmitting} />\n      </div>\n\n      <button type=\"submit\" disabled={isSubmitting}>\n        {isSubmitting ? 'Creating…' : 'Create Playlist'}\n      </button>\n    </form>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/api/use-playlists-query.tsx",
    "content": "import { keepPreviousData, useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '../../../shared/api/client.ts'\nimport { requestWrapper } from '../../../shared/api/request-wrapper.ts'\nimport type { SchemaGetPlaylistsRequestPayload } from '../../../shared/api/schema.ts'\n\nexport const playlistListKey = (p: Partial<SchemaGetPlaylistsRequestPayload> = {}) => {\n  const {\n    pageNumber = 1,\n    pageSize = 20,\n    search = '',\n    sortBy = 'publishedAt',\n    sortDirection = 'desc',\n    tagsIds = [],\n    userId = null,\n    trackId = null,\n  } = p\n\n  return [\n    'playlists',\n    {\n      pageNumber,\n      pageSize,\n      search,\n      sortBy,\n      sortDirection,\n      tagsIds: [...tagsIds].sort(),\n      userId,\n      trackId,\n    } as SchemaGetPlaylistsRequestPayload,\n  ] as const // даёт key-tuple с readonly типами\n}\n\nexport function usePlaylistsQuery(search: string, pageNumber: number, userId: string | undefined) {\n  const query = useQuery({\n    queryKey: playlistListKey({\n      search,\n      pageNumber,\n      userId,\n    }),\n    queryFn: () => {\n      return requestWrapper(\n        getClient().GET('/playlists', {\n          params: {\n            query: {\n              search: search && undefined,\n              pageNumber,\n              pageSize: 5,\n              userId,\n            },\n          },\n        })\n      )\n    },\n    placeholderData: keepPreviousData,\n  })\n  return query\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/edit-playlist-form/edit-playlist-form.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useEffect } from 'react'\nimport { type SubmitHandler, useForm } from 'react-hook-form'\n\nimport { getClient } from '../../../shared/api/client'\nimport { queryErrorHandlerForRHFFactory } from '../../../shared/api/query-error-handler-for-rhf-factory.ts'\nimport { requestWrapper } from '../../../shared/api/request-wrapper.ts'\nimport type { components } from '../../../shared/api/schema'\n\ntype Props = {\n  classNames: string\n  playlistId: string | null\n  onCancelEditing: () => void\n}\n\ntype UpdatePlaylistRequestPayload = components['schemas']['UpdatePlaylistRequestPayload']\n\nexport const EditPlaylistForm = ({ playlistId, onCancelEditing, classNames }: Props) => {\n  const queryClient = useQueryClient()\n\n  /* 1. Загружаем детали плейлиста */\n  const { data: playlistResp, isPending: isPlaylistPending } = useQuery({\n    queryKey: ['playlists', 'details', playlistId],\n    queryFn: ({ signal }) =>\n      getClient().GET('/playlists/{playlistId}', {\n        params: { path: { playlistId: playlistId! } },\n        signal,\n      }),\n    enabled: Boolean(playlistId),\n  })\n\n  /* 2. useForm */\n  const {\n    register,\n    handleSubmit,\n    reset,\n    setError,\n    formState: { isSubmitting, errors },\n  } = useForm<UpdatePlaylistRequestPayload>({\n    defaultValues: { title: '', description: '' }, // дефолты\n  })\n\n  /* 3. Сброс/инициализация формы, когда данные загрузились */\n  useEffect(() => {\n    if (playlistResp?.data) {\n      const { title = '', description = '' } = playlistResp.data.data.attributes\n      reset({ title, description })\n    }\n  }, [playlistResp, reset])\n\n  /* 4. Мутация «обновить плейлист» */\n  const { mutate, isPending } = useMutation({\n    mutationFn: (body: UpdatePlaylistRequestPayload) =>\n      requestWrapper(\n        getClient().PUT('/playlists/{playlistId}', {\n          body: { ...body, tagIds: [] },\n          params: { path: { playlistId: playlistId! } },\n        })\n      ),\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: ['playlists'],\n      })\n      onCancelEditing?.()\n    },\n    onError: queryErrorHandlerForRHFFactory({ setError }),\n  })\n\n  /* 5. Сабмит формы */\n  const onSubmit: SubmitHandler<UpdatePlaylistRequestPayload> = (values) => {\n    if (!playlistId) return\n    mutate(values)\n  }\n\n  if (!playlistId) return null\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)} className={classNames}>\n      <h2>Редактировать плейлист</h2>\n\n      <p>\n        <label>\n          <input\n            {...register('title')}\n            placeholder=\"Title\"\n            disabled={isPending || isPlaylistPending || isSubmitting}\n          />\n        </label>\n      </p>\n      {errors.title && <p>{errors.title.message}</p>}\n      <p>\n        <label>\n          <textarea\n            {...register('description')}\n            placeholder=\"Description\"\n            disabled={isPending || isPlaylistPending || isSubmitting}\n          />\n        </label>\n      </p>\n      {errors.description && <p>{errors.description.message}</p>}\n\n      <button type=\"submit\" disabled={isPending || isPlaylistPending || isSubmitting}>\n        {isPending ? 'Сохраняем…' : 'Сохранить'}\n      </button>\n\n      {errors.root?.server && <p>{errors.root.server.message}</p>}\n\n      <button onClick={() => onCancelEditing?.()}>Cancel</button>\n    </form>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/list/paginated-playlists.module.css",
    "content": ".cardBox {\n  width: 300px;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: start;\n}\n\n.title {\n  word-break: break-all;\n}\n\n.deletePlaylistButton {\n  all: unset;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/list/paginated-playlists.tsx",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { useEffect, useState } from 'react'\n\nimport { usePlaylistsPublicControllerGetPlaylists } from '@/shared/api/orval/playlists-public/playlists-public.ts'\nimport { Pagination } from '@/shared/ui/pagination/pagination.tsx'\n\nimport { getClient } from '../../../shared/api/client.ts'\nimport type {\n  SchemaGetPlaylistOutput,\n  SchemaGetPlaylistsOutput,\n} from '../../../shared/api/schema.ts'\nimport { getSharedSocket } from '../../../shared/api/socket.ts'\nimport { useMeQuery } from '../../auth/api/use-me.query.ts'\nimport { playlistListKey, usePlaylistsQuery } from '../api/use-playlists-query.tsx'\nimport { PlaylistCover } from '../playlist-cover/playlist-cover.tsx'\nimport styles from './paginated-playlists.module.css'\n\ntype Props = {\n  classNames?: string\n  userId?: string\n  onPlaylistSelected?: (playlistId: string) => void\n}\n\nexport type PlaylistCreatedEventPayload = SchemaGetPlaylistOutput\n\nexport const PlaylistCreatedEventName = 'tracks.playlist-created'\nexport type PlaylistCreatedEvent = {\n  type: typeof PlaylistCreatedEventName\n  payload: PlaylistCreatedEventPayload\n}\n\nexport const PaginatedPlaylists = ({ userId, onPlaylistSelected, classNames }: Props) => {\n  const [search, setSearch] = useState('')\n  const [pageNumber, setPageNumber] = useState(1)\n\n  const { data: meData } = useMeQuery()\n  // const query = usePlaylistsQuery(search, pageNumber, userId)\n  const query = usePlaylistsPublicControllerGetPlaylists({ search, pageNumber, userId })\n\n  const queryClient = useQueryClient()\n\n  useEffect(() => {\n    const socket = getSharedSocket(import.meta.env.VITE_AUTH_TOKEN)\n\n    socket.on(PlaylistCreatedEventName, (data: PlaylistCreatedEvent) => {\n      queryClient.setQueryData(\n        playlistListKey({ search, pageNumber: 1, userId: undefined }),\n        (oldData: SchemaGetPlaylistsOutput) => {\n          return {\n            data: [data.payload.data, ...oldData.data],\n            meta: oldData.meta,\n          } as SchemaGetPlaylistsOutput\n        }\n      )\n    })\n  }, [])\n\n  const { mutate: deletePlaylist } = useMutation({\n    mutationFn: (playlistId: string) =>\n      getClient().DELETE('/playlists/{playlistId}', {\n        params: {\n          path: {\n            playlistId,\n          },\n        },\n      }),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['playlists'] })\n    },\n    // onError: (err: unknown) =>\n    //     showErrorToast(\"Ошибка при создании плейлиста\", err),\n  })\n\n  console.log('Playlists rendered')\n\n  if (query.isPending) {\n    return <span>Loading...</span>\n  }\n\n  if (query.isError) {\n    return <span>Error: {query.error.message}</span>\n  }\n\n  return (\n    <div className={classNames}>\n      <div>\n        <input\n          value={search}\n          onChange={(e) => setSearch(e.currentTarget.value)}\n          placeholder={'search...'}\n        />\n      </div>\n      <hr />\n      <Pagination\n        current={pageNumber}\n        pagesCount={query.data!.meta.pagesCount || 0}\n        changePageNumber={setPageNumber}\n        isFetching={query.isFetching}\n      />\n\n      <ul>\n        {query.data!.data.map((playlist) => (\n          <li\n            className={styles.cardBox}\n            key={playlist.id}\n            onClick={(e) => {\n              if (e.target === e.currentTarget) {\n                onPlaylistSelected?.(playlist.id)\n              }\n            }}>\n            <div className={styles.row}>\n              <PlaylistCover\n                images={playlist.attributes.images}\n                playlistId={playlist.id}\n                editable={playlist.attributes.user.id === meData?.userId}\n              />\n              {meData?.userId === playlist.attributes.user.id && (\n                <button\n                  className={styles.deletePlaylistButton}\n                  onClick={() => deletePlaylist(playlist.id)}\n                  title={'Delete playlist'}\n                  aria-label={'Delete playlist'}>\n                  🗑️\n                </button>\n              )}\n            </div>\n\n            <h3 className={styles.title}>{playlist.attributes.title}</h3>\n\n            <hr />\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/list/playlists.tsx",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { useState } from 'react'\n\nimport { getClient } from '@/shared/api'\n\nexport const Playlists = ({ filtersEnabled = false }: { filtersEnabled: boolean }) => {\n  const [search, setSearch] = useState('')\n\n  const query = useQuery({\n    queryKey: ['playlists', search],\n    queryFn: () => {\n      return getClient().GET('/playlists', {\n        params: {\n          query: {\n            search,\n          },\n        },\n      })\n    },\n  })\n\n  console.log('Playlists rendered')\n\n  if (query.isPending) {\n    return <span>Loading...</span>\n  }\n\n  if (query.isError) {\n    return <span>Error: {query.error.message}</span>\n  }\n\n  return (\n    <div>\n      <QueryStatus query={query} />\n      {filtersEnabled && (\n        <div>\n          <input value={search} onChange={(e) => setSearch(e.currentTarget.value)} />\n        </div>\n      )}\n      <ul>\n        {query.data.data!.data.map((playlist) => (\n          <li key={playlist.id}>{playlist.attributes.title}</li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n\nfunction QueryStatus(props: any) {\n  return (\n    <div>\n      <div>status: {props.query.status}</div>\n      <div>fetchStatus: {props.query.fetchStatus}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/playlist-cover/playlist-cover.module.css",
    "content": ".container {\n  margin-bottom: 30px;\n}\n\n.cover {\n  width: 100px;\n  height: 100px;\n  object-fit: cover;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/features/playlists/playlist-cover/playlist-cover.tsx",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport type { ChangeEvent } from 'react'\n\nimport type { components } from '@/shared/api'\nimport { getClient } from '@/shared/api'\n\nimport noCover from '../../../assets/img/no-cover.png'\nimport s from './playlist-cover.module.css'\n\ntype PlaylistImagesOutputDTO = components['schemas']['PlaylistImagesOutputDTO']\n\ntype Props = {\n  editable?: boolean\n  images: PlaylistImagesOutputDTO\n  playlistId: string\n}\n\nexport const PlaylistCover = ({ images, playlistId, editable = false }: Props) => {\n  const queryClient = useQueryClient()\n  const { mutate } = useMutation({\n    mutationFn: (args: { file: File }) => {\n      const { file } = args\n      const formData = new FormData()\n      formData.append('file', file)\n      return getClient().POST('/playlists/{playlistId}/images/main', {\n        params: { path: { playlistId } },\n        body: formData as unknown as { file: string },\n      })\n    },\n    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['playlists'] }),\n    // onError: (err: unknown) => showErrorToast(\"Ошибка при загрузке изображения\", err),\n  })\n\n  const uploadCoverHandler = (event: ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0]\n    mutate({ file: file! })\n  }\n\n  const originalCover = images.main?.find((img) => img.type === 'original')\n\n  return (\n    <div className={s.container}>\n      <img\n        src={originalCover ? originalCover.url : noCover}\n        alt={'no cover image'}\n        className={s.cover}\n      />\n      {editable && (\n        <div>\n          <input\n            type=\"file\"\n            accept=\"image/jpeg,image/png,image/gif\"\n            onChange={uploadCoverHandler}\n          />\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/pages/auth/index.tsx",
    "content": "export { OauthCallbackPage } from './ui/oauth-callback-page.tsx'\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/pages/auth/ui/oauth-callback-page.tsx",
    "content": "import { useEffect } from 'react'\n\nexport function OauthCallbackPage() {\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const code = url.searchParams.get('code') // или code/state, если flow другой\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, '*') // Лучше заменить \"*\" на точный origin\n    }\n\n    window.close()\n  }, [])\n\n  return <p>Logging you in...</p>\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/pages/playlists/ui/my-playlists/my-playlists-page.module.css",
    "content": ".playlistsBox {\n  display: flex;\n  gap: 50px;\n}\n\n.playlistColumn {\n  width: 600px;\n  flex-shrink: 0;\n  padding-top: 10px;\n}\n\n.editFormColumn {\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/pages/playlists/ui/my-playlists/my-playlists-page.tsx",
    "content": "import { Navigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { AddPlaylistForm } from '@/features/playlists/add-playlist-form/add-playlist-form.tsx'\nimport { EditPlaylistForm } from '@/features/playlists/edit-playlist-form/edit-playlist-form.tsx'\nimport { PaginatedPlaylists } from '@/features/playlists/list/paginated-playlists.tsx'\n\nimport styles from './my-playlists-page.module.css'\n\nexport function MyPlaylistsPage() {\n  const { data, isLoading } = useMeQuery()\n  const [editingPlaylistId, setEditingPlaylistId] = useState<string | null>(null)\n\n  if (isLoading) return <span>loading...</span>\n\n  if (!data) {\n    // acts like React-Router’s <Navigate> / Next.js <Redirect>\n    return <Navigate to=\"/\" replace />\n  }\n\n  return (\n    <div>\n      <h3>My Playlists</h3>\n      <AddPlaylistForm />\n      <div className={styles.playlistsBox}>\n        <PaginatedPlaylists\n          onPlaylistSelected={setEditingPlaylistId}\n          userId={data.userId}\n          classNames={styles.playlistColumn}\n        />\n        <EditPlaylistForm\n          playlistId={editingPlaylistId}\n          onCancelEditing={() => setEditingPlaylistId(null)}\n          classNames={styles.editFormColumn}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/pages/playlists/ui/playlists-with-filters-page.tsx",
    "content": "import { PaginatedPlaylists } from '@/features/playlists/list/paginated-playlists.tsx'\n\nexport function PlaylistsWithFiltersPage() {\n  return (\n    <div>\n      <PaginatedPlaylists />\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/client.ts",
    "content": "import createClient, { type Middleware } from 'openapi-fetch'\n\nimport type { paths } from './schema.ts'\n\nconst config = {\n  baseURL: null as string | null,\n  apiKey: null as string | null,\n  getAccessToken: null as (() => Promise<string | null>) | null,\n  saveAccessToken: null as ((accessToken: string | null) => Promise<void>) | null,\n  getRefreshToken: null as (() => Promise<string | null>) | null,\n  saveRefreshToken: null as ((refreshToken: string | null) => Promise<void>) | null,\n}\n\nexport const setClientConfig = (newConfig: Partial<typeof config>) => {\n  Object.assign(config, newConfig)\n  _client = undefined // пере-инициализируем\n}\n\nexport const getClientConfig = () => ({ ...config })\n\n/* ------------------------------------------------------------------ */\n/* 2.  Mutex для refresh-а                                             */\n/* ------------------------------------------------------------------ */\nlet refreshPromise: Promise<string> | null = null\n\nfunction makeRefreshToken(): Promise<string> {\n  if (!refreshPromise) {\n    // 1) создаём «замок» сразу\n    refreshPromise = (async (): Promise<string> => {\n      const refreshToken = await config.getRefreshToken!()\n      if (!refreshToken) throw new Error('No refresh token')\n\n      const res = await fetch(`${config.baseURL}/auth/refresh`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'API-KEY': config.apiKey!,\n        },\n        body: JSON.stringify({ refreshToken }),\n      })\n      if (res.status !== 201) throw new Error('Refresh failed')\n\n      const { accessToken, refreshToken: newRT } = await res.json()\n      await config.saveAccessToken!(accessToken)\n      await config.saveRefreshToken!(newRT)\n\n      return accessToken\n    })().finally(() => {\n      refreshPromise = null // 2) снимаем «замок»\n    })\n  }\n\n  return refreshPromise\n}\n\nconst authMiddleware: Middleware = {\n  /* ---------- REQUEST -------------------------------------------------- */\n  async onRequest({ request }) {\n    request.headers.set('API-KEY', config.apiKey!)\n\n    const token = await config.getAccessToken?.()\n    if (token) request.headers.set('Authorization', `Bearer ${token}`)\n    ;(request as any)._retryClone = request.clone()\n\n    return request\n  },\n  async onResponse({ request, response }) {\n    const req = request as Request & { _retry: boolean }\n\n    if (response.status !== 401 || request.url.includes('/auth/refresh')) {\n      return response // всё ок\n    }\n\n    // уже пытались? -> отдаём 401 наружу, чтобы не зациклиться\n    if (req._retry) return response\n    req._retry = true\n\n    try {\n      const newToken = await makeRefreshToken()\n\n      // повторяем исходный запрос с новым токеном\n      const orig = (req as any)._retryClone as Request // клон с целым body\n      const retry = new Request(orig, { headers: new Headers(orig.headers) })\n      retry.headers.set('Authorization', `Bearer ${newToken}`)\n      return await fetch(retry)\n    } catch (error) {\n      console.log(error)\n      await config.saveAccessToken!(null)\n      await config.saveRefreshToken!(null)\n      return response\n    }\n  },\n}\n\nlet _client: ReturnType<typeof createClient<paths>> | undefined\n\nexport const getClient = () => {\n  if (_client) return _client\n\n  if (!config.baseURL || !config.apiKey) {\n    console.error('call setClientConfig to setup api')\n    throw new Error('call setClientConfig to setup api')\n  }\n\n  const client = createClient<paths>({ baseUrl: config.baseURL })\n  client.use(authMiddleware)\n  _client = client\n  return _client\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/index.ts",
    "content": "export { getClient, setClientConfig } from './client'\nexport * from './schema'\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/json-api-error.ts",
    "content": "export interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport type ExtractError<T> = T extends { error?: infer E } ? E : unknown\n\n/* --- типы ошибок, совпадающие с фильтром -------------------------------- */\nexport interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport function isJsonApiErrorDocument(error: unknown): error is JsonApiErrorDocument {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    // @ts-expect-error type no matter\n    Array.isArray(error.errors)\n  )\n}\n\nexport function parseJsonApiErrors(errorDoc: JsonApiErrorDocument): {\n  fieldErrors: Record<string, string>\n  globalErrors: string[]\n} {\n  const fieldErrors: Record<string, string> = {}\n  const globalErrors: string[] = []\n\n  for (const err of errorDoc.errors) {\n    const msg = err.detail ?? err.title ?? 'Unknown error'\n    const ptr = err.source?.pointer\n    if (ptr) {\n      // убираем префикс JSON:API\n      const field = ptr.replace(/^\\/data\\/attributes\\//, '')\n      fieldErrors[field] = msg\n    } else {\n      globalErrors.push(msg)\n    }\n  }\n\n  return { fieldErrors, globalErrors }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/artists/artists.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nimport type {\n  DataTag,\n  DefinedInitialDataOptions,\n  DefinedUseQueryResult,\n  MutationFunction,\n  QueryClient,\n  QueryFunction,\n  QueryKey,\n  UndefinedInitialDataOptions,\n  UseMutationOptions,\n  UseMutationResult,\n  UseQueryOptions,\n  UseQueryResult,\n} from '@tanstack/react-query'\nimport { useMutation, useQuery } from '@tanstack/react-query'\n\nimport { customInstance } from '.././custom-instance'\nimport type {\n  ArtistsControllerSearchArtistParams,\n  CreateArtistRequestPayload,\n  GetArtistOutput,\n} from '../musicfun.schemas'\n\ntype SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]\n\n/**\n * @summary Create a new artist\n */\nexport const artistsControllerCreateArtist = (\n  createArtistRequestPayload: CreateArtistRequestPayload,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetArtistOutput>(\n    {\n      url: `/artists`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: createArtistRequestPayload,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getArtistsControllerCreateArtistMutationOptions = <\n  TError = null | null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof artistsControllerCreateArtist>>,\n    TError,\n    { data: CreateArtistRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof artistsControllerCreateArtist>>,\n  TError,\n  { data: CreateArtistRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['artistsControllerCreateArtist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof artistsControllerCreateArtist>>,\n    { data: CreateArtistRequestPayload }\n  > = (props) => {\n    const { data } = props ?? {}\n\n    return artistsControllerCreateArtist(data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type ArtistsControllerCreateArtistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof artistsControllerCreateArtist>>\n>\nexport type ArtistsControllerCreateArtistMutationBody = CreateArtistRequestPayload\nexport type ArtistsControllerCreateArtistMutationError = null | null | null | null\n\n/**\n * @summary Create a new artist\n */\nexport const useArtistsControllerCreateArtist = <\n  TError = null | null | null | null,\n  TContext = unknown,\n>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof artistsControllerCreateArtist>>,\n      TError,\n      { data: CreateArtistRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof artistsControllerCreateArtist>>,\n  TError,\n  { data: CreateArtistRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getArtistsControllerCreateArtistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Search artists by substring\n */\nexport const artistsControllerSearchArtist = (\n  params: ArtistsControllerSearchArtistParams,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetArtistOutput[]>(\n    { url: `/artists/search`, method: 'GET', params, signal },\n    options\n  )\n}\n\nexport const getArtistsControllerSearchArtistQueryKey = (\n  params?: ArtistsControllerSearchArtistParams\n) => {\n  return [`/artists/search`, ...(params ? [params] : [])] as const\n}\n\nexport const getArtistsControllerSearchArtistQueryOptions = <\n  TData = Awaited<ReturnType<typeof artistsControllerSearchArtist>>,\n  TError = unknown,\n>(\n  params: ArtistsControllerSearchArtistParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof artistsControllerSearchArtist>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  }\n) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey = queryOptions?.queryKey ?? getArtistsControllerSearchArtistQueryKey(params)\n\n  const queryFn: QueryFunction<Awaited<ReturnType<typeof artistsControllerSearchArtist>>> = ({\n    signal,\n  }) => artistsControllerSearchArtist(params, requestOptions, signal)\n\n  return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof artistsControllerSearchArtist>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type ArtistsControllerSearchArtistQueryResult = NonNullable<\n  Awaited<ReturnType<typeof artistsControllerSearchArtist>>\n>\nexport type ArtistsControllerSearchArtistQueryError = unknown\n\nexport function useArtistsControllerSearchArtist<\n  TData = Awaited<ReturnType<typeof artistsControllerSearchArtist>>,\n  TError = unknown,\n>(\n  params: ArtistsControllerSearchArtistParams,\n  options: {\n    query: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof artistsControllerSearchArtist>>, TError, TData>\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof artistsControllerSearchArtist>>,\n          TError,\n          Awaited<ReturnType<typeof artistsControllerSearchArtist>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useArtistsControllerSearchArtist<\n  TData = Awaited<ReturnType<typeof artistsControllerSearchArtist>>,\n  TError = unknown,\n>(\n  params: ArtistsControllerSearchArtistParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof artistsControllerSearchArtist>>, TError, TData>\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof artistsControllerSearchArtist>>,\n          TError,\n          Awaited<ReturnType<typeof artistsControllerSearchArtist>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useArtistsControllerSearchArtist<\n  TData = Awaited<ReturnType<typeof artistsControllerSearchArtist>>,\n  TError = unknown,\n>(\n  params: ArtistsControllerSearchArtistParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof artistsControllerSearchArtist>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary Search artists by substring\n */\n\nexport function useArtistsControllerSearchArtist<\n  TData = Awaited<ReturnType<typeof artistsControllerSearchArtist>>,\n  TError = unknown,\n>(\n  params: ArtistsControllerSearchArtistParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof artistsControllerSearchArtist>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getArtistsControllerSearchArtistQueryOptions(params, options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Delete an artist by ID\n */\nexport const artistsControllerDeleteArtist = (\n  id: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/artists/${id}`, method: 'DELETE' }, options)\n}\n\nexport const getArtistsControllerDeleteArtistMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof artistsControllerDeleteArtist>>,\n    TError,\n    { id: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof artistsControllerDeleteArtist>>,\n  TError,\n  { id: string },\n  TContext\n> => {\n  const mutationKey = ['artistsControllerDeleteArtist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof artistsControllerDeleteArtist>>,\n    { id: string }\n  > = (props) => {\n    const { id } = props ?? {}\n\n    return artistsControllerDeleteArtist(id, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type ArtistsControllerDeleteArtistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof artistsControllerDeleteArtist>>\n>\n\nexport type ArtistsControllerDeleteArtistMutationError = null | null\n\n/**\n * @summary Delete an artist by ID\n */\nexport const useArtistsControllerDeleteArtist = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof artistsControllerDeleteArtist>>,\n      TError,\n      { id: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof artistsControllerDeleteArtist>>,\n  TError,\n  { id: string },\n  TContext\n> => {\n  const mutationOptions = getArtistsControllerDeleteArtistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/authentication/authentication.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nimport type {\n  DataTag,\n  DefinedInitialDataOptions,\n  DefinedUseQueryResult,\n  MutationFunction,\n  QueryClient,\n  QueryFunction,\n  QueryKey,\n  UndefinedInitialDataOptions,\n  UseMutationOptions,\n  UseMutationResult,\n  UseQueryOptions,\n  UseQueryResult,\n} from '@tanstack/react-query'\nimport { useMutation, useQuery } from '@tanstack/react-query'\n\nimport { customInstance } from '.././custom-instance'\nimport type {\n  AuthControllerOauthRedirectParams,\n  BadRequestException,\n  GetMeOutput,\n  LoginRequestPayload,\n  LogoutRequestPayload,\n  RefreshOutput,\n  RefreshRequestPayload,\n  UnauthorizedException,\n} from '../musicfun.schemas'\n\ntype SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]\n\n/**\n * The callback URL to redirect after granting access, <a target=\"_blank\" href=\"https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\">https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid</a>\n * @summary OAuth redirect\n */\nexport const authControllerOauthRedirect = (\n  params: AuthControllerOauthRedirectParams,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<null>(\n    { url: `/auth/oauth-redirect`, method: 'GET', params, signal },\n    options\n  )\n}\n\nexport const getAuthControllerOauthRedirectQueryKey = (\n  params?: AuthControllerOauthRedirectParams\n) => {\n  return [`/auth/oauth-redirect`, ...(params ? [params] : [])] as const\n}\n\nexport const getAuthControllerOauthRedirectQueryOptions = <\n  TData = Awaited<ReturnType<typeof authControllerOauthRedirect>>,\n  TError = unknown,\n>(\n  params: AuthControllerOauthRedirectParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof authControllerOauthRedirect>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  }\n) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey = queryOptions?.queryKey ?? getAuthControllerOauthRedirectQueryKey(params)\n\n  const queryFn: QueryFunction<Awaited<ReturnType<typeof authControllerOauthRedirect>>> = ({\n    signal,\n  }) => authControllerOauthRedirect(params, requestOptions, signal)\n\n  return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof authControllerOauthRedirect>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type AuthControllerOauthRedirectQueryResult = NonNullable<\n  Awaited<ReturnType<typeof authControllerOauthRedirect>>\n>\nexport type AuthControllerOauthRedirectQueryError = unknown\n\nexport function useAuthControllerOauthRedirect<\n  TData = Awaited<ReturnType<typeof authControllerOauthRedirect>>,\n  TError = unknown,\n>(\n  params: AuthControllerOauthRedirectParams,\n  options: {\n    query: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof authControllerOauthRedirect>>, TError, TData>\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof authControllerOauthRedirect>>,\n          TError,\n          Awaited<ReturnType<typeof authControllerOauthRedirect>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useAuthControllerOauthRedirect<\n  TData = Awaited<ReturnType<typeof authControllerOauthRedirect>>,\n  TError = unknown,\n>(\n  params: AuthControllerOauthRedirectParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof authControllerOauthRedirect>>, TError, TData>\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof authControllerOauthRedirect>>,\n          TError,\n          Awaited<ReturnType<typeof authControllerOauthRedirect>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useAuthControllerOauthRedirect<\n  TData = Awaited<ReturnType<typeof authControllerOauthRedirect>>,\n  TError = unknown,\n>(\n  params: AuthControllerOauthRedirectParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof authControllerOauthRedirect>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary OAuth redirect\n */\n\nexport function useAuthControllerOauthRedirect<\n  TData = Awaited<ReturnType<typeof authControllerOauthRedirect>>,\n  TError = unknown,\n>(\n  params: AuthControllerOauthRedirectParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof authControllerOauthRedirect>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getAuthControllerOauthRedirectQueryOptions(params, options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Log in using the code received after OAuth authorization redirect\n */\nexport const authControllerLogin = (\n  loginRequestPayload: LoginRequestPayload,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<RefreshOutput>(\n    {\n      url: `/auth/login`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: loginRequestPayload,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getAuthControllerLoginMutationOptions = <\n  TError = BadRequestException | UnauthorizedException,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof authControllerLogin>>,\n    TError,\n    { data: LoginRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof authControllerLogin>>,\n  TError,\n  { data: LoginRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['authControllerLogin']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof authControllerLogin>>,\n    { data: LoginRequestPayload }\n  > = (props) => {\n    const { data } = props ?? {}\n\n    return authControllerLogin(data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type AuthControllerLoginMutationResult = NonNullable<\n  Awaited<ReturnType<typeof authControllerLogin>>\n>\nexport type AuthControllerLoginMutationBody = LoginRequestPayload\nexport type AuthControllerLoginMutationError = BadRequestException | UnauthorizedException\n\n/**\n * @summary Log in using the code received after OAuth authorization redirect\n */\nexport const useAuthControllerLogin = <\n  TError = BadRequestException | UnauthorizedException,\n  TContext = unknown,\n>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof authControllerLogin>>,\n      TError,\n      { data: LoginRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof authControllerLogin>>,\n  TError,\n  { data: LoginRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getAuthControllerLoginMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Refresh refresh/access token pair\n */\nexport const authControllerRefresh = (\n  refreshRequestPayload: RefreshRequestPayload,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<RefreshOutput>(\n    {\n      url: `/auth/refresh`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: refreshRequestPayload,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getAuthControllerRefreshMutationOptions = <\n  TError = UnauthorizedException,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof authControllerRefresh>>,\n    TError,\n    { data: RefreshRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof authControllerRefresh>>,\n  TError,\n  { data: RefreshRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['authControllerRefresh']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof authControllerRefresh>>,\n    { data: RefreshRequestPayload }\n  > = (props) => {\n    const { data } = props ?? {}\n\n    return authControllerRefresh(data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type AuthControllerRefreshMutationResult = NonNullable<\n  Awaited<ReturnType<typeof authControllerRefresh>>\n>\nexport type AuthControllerRefreshMutationBody = RefreshRequestPayload\nexport type AuthControllerRefreshMutationError = UnauthorizedException\n\n/**\n * @summary Refresh refresh/access token pair\n */\nexport const useAuthControllerRefresh = <TError = UnauthorizedException, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof authControllerRefresh>>,\n      TError,\n      { data: RefreshRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof authControllerRefresh>>,\n  TError,\n  { data: RefreshRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getAuthControllerRefreshMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Deactivate refresh token\n */\nexport const authControllerLogout = (\n  logoutRequestPayload: LogoutRequestPayload,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<null>(\n    {\n      url: `/auth/logout`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: logoutRequestPayload,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getAuthControllerLogoutMutationOptions = <\n  TError = unknown,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof authControllerLogout>>,\n    TError,\n    { data: LogoutRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof authControllerLogout>>,\n  TError,\n  { data: LogoutRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['authControllerLogout']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof authControllerLogout>>,\n    { data: LogoutRequestPayload }\n  > = (props) => {\n    const { data } = props ?? {}\n\n    return authControllerLogout(data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type AuthControllerLogoutMutationResult = NonNullable<\n  Awaited<ReturnType<typeof authControllerLogout>>\n>\nexport type AuthControllerLogoutMutationBody = LogoutRequestPayload\nexport type AuthControllerLogoutMutationError = unknown\n\n/**\n * @summary Deactivate refresh token\n */\nexport const useAuthControllerLogout = <TError = unknown, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof authControllerLogout>>,\n      TError,\n      { data: LogoutRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof authControllerLogout>>,\n  TError,\n  { data: LogoutRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getAuthControllerLogoutMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Get current user by access token\n */\nexport const authControllerGetMe = (\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetMeOutput>({ url: `/auth/me`, method: 'GET', signal }, options)\n}\n\nexport const getAuthControllerGetMeQueryKey = () => {\n  return [`/auth/me`] as const\n}\n\nexport const getAuthControllerGetMeQueryOptions = <\n  TData = Awaited<ReturnType<typeof authControllerGetMe>>,\n  TError = null,\n>(options?: {\n  query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof authControllerGetMe>>, TError, TData>>\n  request?: SecondParameter<typeof customInstance>\n}) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey = queryOptions?.queryKey ?? getAuthControllerGetMeQueryKey()\n\n  const queryFn: QueryFunction<Awaited<ReturnType<typeof authControllerGetMe>>> = ({ signal }) =>\n    authControllerGetMe(requestOptions, signal)\n\n  return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof authControllerGetMe>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type AuthControllerGetMeQueryResult = NonNullable<\n  Awaited<ReturnType<typeof authControllerGetMe>>\n>\nexport type AuthControllerGetMeQueryError = null\n\nexport function useAuthControllerGetMe<\n  TData = Awaited<ReturnType<typeof authControllerGetMe>>,\n  TError = null,\n>(\n  options: {\n    query: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof authControllerGetMe>>, TError, TData>\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof authControllerGetMe>>,\n          TError,\n          Awaited<ReturnType<typeof authControllerGetMe>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useAuthControllerGetMe<\n  TData = Awaited<ReturnType<typeof authControllerGetMe>>,\n  TError = null,\n>(\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof authControllerGetMe>>, TError, TData>\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof authControllerGetMe>>,\n          TError,\n          Awaited<ReturnType<typeof authControllerGetMe>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useAuthControllerGetMe<\n  TData = Awaited<ReturnType<typeof authControllerGetMe>>,\n  TError = null,\n>(\n  options?: {\n    query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof authControllerGetMe>>, TError, TData>>\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary Get current user by access token\n */\n\nexport function useAuthControllerGetMe<\n  TData = Awaited<ReturnType<typeof authControllerGetMe>>,\n  TError = null,\n>(\n  options?: {\n    query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof authControllerGetMe>>, TError, TData>>\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getAuthControllerGetMeQueryOptions(options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/custom-instance.ts",
    "content": "// custom-instance.ts\n\nimport Axios, { type AxiosRequestConfig } from 'axios'\n\nimport { apiBaseUrl, apiKey } from '@/shared/config/api.config.ts'\n\nexport const AXIOS_INSTANCE = Axios.create({\n  baseURL: apiBaseUrl,\n  headers: {\n    'api-key': apiKey,\n  },\n}) // use your own URL here or environment variable\n\nexport const customInstance = <T>(\n  config: AxiosRequestConfig,\n  options?: AxiosRequestConfig\n): Promise<T> => {\n  const source = Axios.CancelToken.source()\n  const promise = AXIOS_INSTANCE({\n    ...config,\n    ...options,\n    cancelToken: source.token,\n  }).then(({ data }) => data)\n\n  // @ts-ignore\n  promise.cancel = () => {\n    source.cancel('Query was cancelled')\n  }\n\n  return promise\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/musicfun.schemas.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nexport interface UserOutputDTO {\n  /** Unique identifier of the user */\n  id: string\n  /** Name of the user */\n  name: string\n}\n\n/**\n * Type of the image size (e.g., original, thumbnail variants)\n */\nexport type ImageSizeType = (typeof ImageSizeType)[keyof typeof ImageSizeType]\n\nexport const ImageSizeType = {\n  original: 'original',\n  thumbnail: 'thumbnail',\n  medium: 'medium',\n} as const\n\nexport interface ImageDto {\n  /** Type of the image size (e.g., original, thumbnail variants) */\n  type: ImageSizeType\n  /** Image width in pixels */\n  width: number\n  /** Image height in pixels */\n  height: number\n  /** Image file size in bytes */\n  fileSize: number\n  /** Full public URL of the image */\n  url: string\n}\n\nexport interface PlaylistImagesOutputDTO {\n  /** Original images and thumbnail previews */\n  main?: ImageDto[]\n}\n\nexport interface GetTagOutput {\n  /** Unique identifier of the tag */\n  id: string\n  /** Original name of the tag */\n  name: string\n}\n\n/**\n * User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike\n */\nexport type ReactionValue = (typeof ReactionValue)[keyof typeof ReactionValue]\n\nexport const ReactionValue = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface PlaylistAttributesDto {\n  /** Title of the playlist */\n  title: string\n  /**\n   * Description of the playlist\n   * @nullable\n   */\n  description: string | null\n  /** Date and time when the playlist was added (ISO 8601) */\n  addedAt: string\n  /** Date and time when the playlist was last updated (ISO 8601) */\n  updatedAt: string\n  /** Order index of the playlist */\n  order: number\n  /** User who created the playlist */\n  user: UserOutputDTO\n  /** Images associated with the playlist */\n  images: PlaylistImagesOutputDTO\n  /** Tags linked to the playlist */\n  tags: GetTagOutput[]\n  /** Total number of likes for this playlist */\n  likesCount: number\n  /** Total number of dislikes for this playlist */\n  dislikesCount: number\n  /** User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n  currentUserReaction: ReactionValue\n}\n\nexport interface PlaylistListItemJsonApiData {\n  /** Unique identifier of the playlist */\n  id: string\n  /** Resource type (should be \"playlists\") */\n  type: string\n  /** Attributes of the playlist resource */\n  attributes: PlaylistAttributesDto\n}\n\nexport interface GetMyPlaylistsOutput {\n  /** Array of playlist resource objects owned by the current user */\n  data: PlaylistListItemJsonApiData[]\n}\n\nexport interface CreatePlaylistRequestPayload {\n  /**\n   * Playlist title (1 to 100 characters)\n   * @minLength 1\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Playlist description (up to 1000 characters)\n   * @maxLength 1000\n   * @nullable\n   */\n  description: string | null\n}\n\nexport interface PlaylistOutputAttributes {\n  /** Title of the playlist */\n  title: string\n  /**\n   * Description of the playlist\n   * @nullable\n   */\n  description: string | null\n  /** Date and time when the playlist was added (ISO 8601) */\n  addedAt: string\n  /** Date and time when the playlist was last updated (ISO 8601) */\n  updatedAt: string\n  /** Order index of the playlist */\n  order: number\n  /** User who created the playlist */\n  user: UserOutputDTO\n  /** Images associated with the playlist */\n  images: PlaylistImagesOutputDTO\n  /** Tags linked to the playlist */\n  tags: GetTagOutput[]\n  /** Total number of likes for this playlist */\n  likesCount: number\n  /** Total number of dislikes for this playlist */\n  dislikesCount: number\n  /** User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n  currentUserReaction: ReactionValue\n}\n\nexport interface PlaylistOutput {\n  /** Unique identifier of the playlist */\n  id: string\n  /** Resource type (should be \"playlists\") */\n  type: string\n  /** Playlist attributes object */\n  attributes: PlaylistOutputAttributes\n}\n\nexport interface GetPlaylistOutput {\n  /** JSON:API single-resource response wrapper */\n  data: PlaylistOutput\n}\n\nexport interface UpdatePlaylistRequestPayload {\n  /**\n   * Playlist title (1 – 100 characters)\n   * @minLength 1\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Playlist description (up to 1000 characters)\n   * @maxLength 1000\n   * @nullable\n   */\n  description: string | null\n  /**\n   * Tag IDs to associate with the playlist (0 – 5 items; [] = clear tags)\n   * @maxItems 5\n   */\n  tagIds: string[]\n}\n\nexport interface ReorderPlaylistsRequestPayload {\n  /**\n   * ID of the playlist after which the current playlist should be inserted. Send null to place the playlist at the beginning of the list.\n   * @nullable\n   */\n  putAfterItemId: string | null\n}\n\nexport interface GetImagesOutput {\n  /** List of original images and thumbnail versions (e.g., original, 320x180, etc.) */\n  main?: ImageDto[]\n}\n\n/**\n * Field by which to sort tracks\n */\nexport type GetTracksRequestPayloadSortBy =\n  (typeof GetTracksRequestPayloadSortBy)[keyof typeof GetTracksRequestPayloadSortBy]\n\nexport const GetTracksRequestPayloadSortBy = {\n  publishedAt: 'publishedAt',\n  likesCount: 'likesCount',\n} as const\n\n/**\n * Sort direction (ascending or descending)\n */\nexport type GetTracksRequestPayloadSortDirection =\n  (typeof GetTracksRequestPayloadSortDirection)[keyof typeof GetTracksRequestPayloadSortDirection]\n\nexport const GetTracksRequestPayloadSortDirection = {\n  asc: 'asc',\n  desc: 'desc',\n} as const\n\n/**\n * Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination.\n */\nexport type GetTracksRequestPayloadPaginationType =\n  (typeof GetTracksRequestPayloadPaginationType)[keyof typeof GetTracksRequestPayloadPaginationType]\n\nexport const GetTracksRequestPayloadPaginationType = {\n  offset: 'offset',\n  cursor: 'cursor',\n} as const\n\nexport interface GetTracksRequestPayload {\n  /**\n   * Page number for pagination (starting from 1)\n   * @minimum 1\n   */\n  pageNumber?: number\n  /**\n   * Page size for pagination (between 1 and 20)\n   * @minimum 1\n   * @maximum 20\n   */\n  pageSize?: number\n  /** Search term for filtering playlists by name */\n  search?: string\n  /** Field by which to sort tracks */\n  sortBy?: GetTracksRequestPayloadSortBy\n  /** Sort direction (ascending or descending) */\n  sortDirection?: GetTracksRequestPayloadSortDirection\n  /** Filter by tag IDs (multiple values allowed) */\n  tagsIds?: string[]\n  /** Filter by artist IDs (multiple values allowed) */\n  artistsIds?: string[]\n  /** Filter by user ID (track creator's ID) */\n  userId?: string\n  /** If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n  includeDrafts?: boolean\n  /** Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination. */\n  paginationType?: GetTracksRequestPayloadPaginationType\n  /**\n   * Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\".\n   * @nullable\n   */\n  cursor?: string | null\n}\n\nexport interface JsonApiErrorSource {\n  /** e.g. \"/data/attributes/field\" */\n  pointer?: string\n  /** e.g. \"?queryParam\" */\n  parameter?: string\n}\n\n/**\n * Application-specific error code\n */\nexport type JsonApiErrorCode = { [key: string]: unknown }\n\n/**\n * Any extra data\n */\nexport type JsonApiErrorMeta = { [key: string]: unknown }\n\nexport interface JsonApiError {\n  /** HTTP status code as a string */\n  status: string\n  /** Application-specific error code */\n  code?: JsonApiErrorCode\n  /** Short, human-readable summary */\n  title?: string\n  /** Detailed explanation */\n  detail?: string\n  /** Pointer to the associated entity in the request */\n  source?: JsonApiErrorSource\n  /** Any extra data */\n  meta?: JsonApiErrorMeta\n}\n\n/**\n * e.g. timestamp, path, traceId, etc.\n */\nexport type JsonApiErrorDocumentMeta = { [key: string]: unknown }\n\nexport interface JsonApiErrorDocument {\n  /** Array of one or more errors */\n  errors: JsonApiError[]\n  /** e.g. timestamp, path, traceId, etc. */\n  meta?: JsonApiErrorDocumentMeta\n}\n\nexport interface AttachmentDto {\n  /** Unique identifier of the entity */\n  id: string\n  /** Date and time when the entity was added */\n  addedAt: string\n  /** Date and time when the entity was last updated */\n  updatedAt: string\n  /** Version number of the entity (for concurrency control) */\n  version: number\n  /** Public URL to access the uploaded file */\n  url: string\n  /** MIME type of the file */\n  contentType: string\n  /** Original filename uploaded by the user */\n  originalName: string\n  /** Size of the file in bytes */\n  fileSize: number\n}\n\n/**\n * 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк\n */\nexport type TrackListItemOutputAttributesCurrentUserReaction =\n  (typeof TrackListItemOutputAttributesCurrentUserReaction)[keyof typeof TrackListItemOutputAttributesCurrentUserReaction]\n\nexport const TrackListItemOutputAttributesCurrentUserReaction = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface TrackListItemOutputAttributes {\n  title: string\n  addedAt: string\n  likesCount: number\n  attachments: AttachmentDto[]\n  images: GetImagesOutput\n  user: UserOutputDTO\n  /** 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк */\n  currentUserReaction: TrackListItemOutputAttributesCurrentUserReaction\n  isPublished: boolean\n  publishedAt?: string\n}\n\nexport interface ArtistRelationship {\n  id: string\n  type: string\n}\n\nexport interface ArtistsRelationship {\n  data: ArtistRelationship[]\n}\n\nexport interface TrackRelationships {\n  artists: ArtistsRelationship\n}\n\nexport interface TrackListItemOutput {\n  id: string\n  type: string\n  attributes: TrackListItemOutputAttributes\n  relationships: TrackRelationships\n}\n\nexport interface JsonApiMetaWithPagingAndCursor {\n  page: number\n  pageSize: number\n  /**\n   * Total count may be absent when using keyset pagination\n   * @nullable\n   */\n  totalCount: number | null\n  /**\n   * Total number of pages\n   * @nullable\n   */\n  pagesCount: number | null\n  /**\n   * Cursor for the next page\n   * @nullable\n   */\n  nextCursor: string | null\n}\n\nexport interface OmitTypeClass {\n  /** Name of the artist */\n  name: string\n}\n\nexport interface IncludedArtistOutput {\n  id: string\n  type: string\n  attributes: OmitTypeClass\n}\n\nexport interface GetTrackListOutput {\n  data: TrackListItemOutput[]\n  meta: JsonApiMetaWithPagingAndCursor\n  included: IncludedArtistOutput[]\n}\n\n/**\n * User reaction: 0 – guest or no reaction; 1 – liked; -1 – disliked\n * @nullable\n */\nexport type PlaylistTrackAttributesCurrentUserReaction =\n  | (typeof PlaylistTrackAttributesCurrentUserReaction)[keyof typeof PlaylistTrackAttributesCurrentUserReaction]\n  | null\n\nexport const PlaylistTrackAttributesCurrentUserReaction = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface PlaylistTrackAttributes {\n  /** Title of the track */\n  title: string\n  /** Order index of the track in the playlist */\n  order: number\n  /** Date and time when the track was added to the playlist (ISO 8601) */\n  addedAt: string\n  /** Date and time when the track was last updated in the playlist (ISO 8601) */\n  updatedAt: string\n  /** Attachments related to the track */\n  attachments: AttachmentDto[]\n  /** Images associated with the track */\n  images: GetImagesOutput\n  /**\n   * User reaction: 0 – guest or no reaction; 1 – liked; -1 – disliked\n   * @nullable\n   */\n  currentUserReaction: PlaylistTrackAttributesCurrentUserReaction\n}\n\nexport interface GetPlaylistTrackListOutputData {\n  id: string\n  type: string\n  attributes: PlaylistTrackAttributes\n  relationships: TrackRelationships\n}\n\nexport interface JsonApiMeta {\n  totalCount: number\n}\n\nexport interface GetPlaylistTrackListOutput {\n  data: GetPlaylistTrackListOutputData[]\n  meta: JsonApiMeta\n  included: IncludedArtistOutput[]\n}\n\nexport interface GetArtistOutput {\n  /** Unique identifier of the artist */\n  id: string\n  /** Name of the artist */\n  name: string\n}\n\n/**\n * User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n */\nexport type TrackDetailsAttributesCurrentUserReaction =\n  (typeof TrackDetailsAttributesCurrentUserReaction)[keyof typeof TrackDetailsAttributesCurrentUserReaction]\n\nexport const TrackDetailsAttributesCurrentUserReaction = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface TrackDetailsAttributes {\n  /**\n   * Track title\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Track lyrics text\n   * @maxLength 5000\n   * @nullable\n   */\n  lyrics?: string | null\n  /**\n   * Release date in ISO 8601 format\n   * @nullable\n   */\n  releaseDate?: string | null\n  /** Date and time when the track was added (ISO 8601) */\n  addedAt: string\n  /** Date and time when the track was last updated (ISO 8601) */\n  updatedAt: string\n  /** Duration of the track in seconds */\n  duration: number\n  /** Total number of likes for this track */\n  likesCount: number\n  /**\n   * Total number of dislikes for this track\n   * @deprecated\n   */\n  dislikesCount: number\n  /** List of attachments related to the track */\n  attachments: AttachmentDto[]\n  /** Images associated with the track */\n  images: GetImagesOutput\n  /** Tags associated with the track */\n  tags: GetTagOutput[]\n  /** Artists associated with the track */\n  artists: GetArtistOutput[]\n  user: UserOutputDTO\n  /** Publication status of the track */\n  isPublished: boolean\n  /**\n   * Publication date in ISO 8601 format\n   * @nullable\n   */\n  publishedAt?: string | null\n  /** User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked */\n  currentUserReaction: TrackDetailsAttributesCurrentUserReaction\n}\n\nexport interface TrackDetailsData {\n  /** Unique identifier of the track */\n  id: string\n  /** Resource type (should be \"tracks\") */\n  type: string\n  /** Detailed attributes of the track resource */\n  attributes: TrackDetailsAttributes\n}\n\nexport interface GetTrackDetailsOutput {\n  /** JSON:API single-track details response wrapper */\n  data: TrackDetailsData\n}\n\nexport type ReactionOutputValue = (typeof ReactionOutputValue)[keyof typeof ReactionOutputValue]\n\nexport const ReactionOutputValue = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface ReactionOutput {\n  objectId: string\n  value: ReactionOutputValue\n  likes: number\n  dislikes: number\n}\n\n/**\n * Field by which to sort playlists\n */\nexport type GetPlaylistsRequestPayloadSortBy =\n  (typeof GetPlaylistsRequestPayloadSortBy)[keyof typeof GetPlaylistsRequestPayloadSortBy]\n\nexport const GetPlaylistsRequestPayloadSortBy = {\n  addedAt: 'addedAt',\n  likesCount: 'likesCount',\n} as const\n\n/**\n * Sort direction (ascending or descending)\n */\nexport type GetPlaylistsRequestPayloadSortDirection =\n  (typeof GetPlaylistsRequestPayloadSortDirection)[keyof typeof GetPlaylistsRequestPayloadSortDirection]\n\nexport const GetPlaylistsRequestPayloadSortDirection = {\n  asc: 'asc',\n  desc: 'desc',\n} as const\n\nexport interface GetPlaylistsRequestPayload {\n  /**\n   * Page number for pagination (starting from 1)\n   * @minimum 1\n   */\n  pageNumber?: number\n  /**\n   * Page size for pagination (between 1 and 20)\n   * @minimum 1\n   * @maximum 20\n   */\n  pageSize?: number\n  /** Search term for filtering playlists by name */\n  search?: string\n  /** Field by which to sort playlists */\n  sortBy?: GetPlaylistsRequestPayloadSortBy\n  /** Sort direction (ascending or descending) */\n  sortDirection?: GetPlaylistsRequestPayloadSortDirection\n  /** Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n  tagsIds?: string[]\n  /** Filter by user ID (playlist creator’s ID) */\n  userId?: string\n  /** Filter by track ID – only playlists containing this track will be returned */\n  trackId?: string\n}\n\nexport interface JsonApiMetaWithPaging {\n  totalCount: number\n  page: number\n  pageSize: number\n  pagesCount: number\n}\n\nexport interface GetPlaylistsOutput {\n  /** Array of playlist resource objects */\n  data: PlaylistListItemJsonApiData[]\n  /** Pagination metadata for the playlists list */\n  meta: JsonApiMetaWithPaging\n}\n\nexport interface ReorderTracksRequestPayload {\n  /**\n   * ID of the track after which the current track should be inserted. Send null to place the track at the beginning of the list.\n   * @nullable\n   */\n  putAfterItemId: string | null\n}\n\nexport interface UpdateTrackRequestPayload {\n  /**\n   * Track title (1 to 100 characters)\n   * @minLength 1\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Track lyrics (up to 5000 characters)\n   * @maxLength 5000\n   * @nullable\n   */\n  lyrics: string | null\n  /**\n   * Release date in ISO 8601 format\n   * @nullable\n   */\n  releaseDate: string | null\n  /**\n   * Array of tag IDs to associate with the track (up to 5)\n   * @maxItems 5\n   */\n  tagIds: string[]\n  /**\n   * Array of artist IDs to associate with the track (up to 5)\n   * @maxItems 5\n   */\n  artistsIds: string[]\n}\n\n/**\n * User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n */\nexport type TrackOutputAttributesCurrentUserReaction =\n  (typeof TrackOutputAttributesCurrentUserReaction)[keyof typeof TrackOutputAttributesCurrentUserReaction]\n\nexport const TrackOutputAttributesCurrentUserReaction = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface TrackOutputAttributes {\n  /**\n   * Track title\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Track lyrics text\n   * @maxLength 5000\n   * @nullable\n   */\n  lyrics?: string | null\n  /**\n   * Release date in ISO 8601 format\n   * @nullable\n   */\n  releaseDate?: string | null\n  /** Date and time when the track was added (ISO 8601) */\n  addedAt: string\n  /** Date and time when the track was last updated (ISO 8601) */\n  updatedAt: string\n  /** Duration of the track in seconds */\n  duration: number\n  /** Total number of likes for this track */\n  likesCount: number\n  /**\n   * Total number of dislikes for this track\n   * @deprecated\n   */\n  dislikesCount: number\n  /** List of attachments related to the track */\n  attachments: AttachmentDto[]\n  /** Images associated with the track */\n  images: GetImagesOutput\n  /** Tags associated with the track */\n  tags: GetTagOutput[]\n  /** Artists associated with the track */\n  artists: GetArtistOutput[]\n  user: UserOutputDTO\n  /** Publication status of the track */\n  isPublished: boolean\n  /**\n   * Publication date in ISO 8601 format\n   * @nullable\n   */\n  publishedAt?: string | null\n  /** User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked */\n  currentUserReaction: TrackOutputAttributesCurrentUserReaction\n}\n\nexport interface TrackOutput {\n  /** Unique identifier of the track */\n  id: string\n  /** Resource type (should be \"tracks\") */\n  type: string\n  /** Attributes of the track resource */\n  attributes: TrackOutputAttributes\n}\n\nexport interface GetTrackOutput {\n  /** JSON:API single-track response wrapper */\n  data: TrackOutput\n}\n\nexport interface AddTrackToPlaylistRequestPayload {\n  /** ID of the track to add to the playlist */\n  trackId: string\n}\n\nexport interface CreateArtistRequestPayload {\n  /**\n   * Artist name (must be between 2 and 30 characters)\n   * @minLength 2\n   * @maxLength 30\n   */\n  name: string\n}\n\nexport interface LoginRequestPayload {\n  /** Authorization code received from OAuth server after redirect */\n  code: string\n  /** Specify the same redirect URI used in the initial OAuth server request */\n  redirectUri: string\n  /** Access token lifetime (default \"3m\"); must be a string like \"60s\", \"3m\", \"2h\", or \"1d\" */\n  accessTokenTTL?: string\n  /** Refresh token lifetime: if true, 30 days; if false, 30 minutes. accessTokenTTL must not exceed the refresh token lifetime */\n  rememberMe: boolean\n}\n\nexport interface RefreshOutput {\n  refreshToken: string\n  accessToken: string\n}\n\nexport interface BadRequestException {\n  [key: string]: unknown\n}\n\nexport interface UnauthorizedException {\n  [key: string]: unknown\n}\n\nexport interface RefreshRequestPayload {\n  refreshToken: string\n}\n\nexport interface LogoutRequestPayload {\n  refreshToken: string\n}\n\nexport interface GetMeOutput {\n  userId: string\n  login: string\n}\n\nexport interface CreateTagRequestPayload {\n  /**\n   * Tag name (2 to 30 characters)\n   * @minLength 2\n   * @maxLength 30\n   */\n  name: string\n}\n\n/**\n * Файл в multipart/form-data\n */\nexport type BinaryFile = Blob\n\nexport type PlaylistsPublicControllerGetPlaylistsParams = {\n  /**\n   * Page number for pagination (starting from 1)\n   * @minimum 1\n   */\n  pageNumber?: number\n  /**\n   * Page size for pagination (between 1 and 20)\n   * @minimum 1\n   * @maximum 20\n   */\n  pageSize?: number\n  /**\n   * Search term for filtering playlists by name\n   */\n  search?: string\n  /**\n   * Field by which to sort playlists\n   */\n  sortBy?: PlaylistsPublicControllerGetPlaylistsSortBy\n  /**\n   * Sort direction (ascending or descending)\n   */\n  sortDirection?: PlaylistsPublicControllerGetPlaylistsSortDirection\n  /**\n   * Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2\n   */\n  tagsIds?: string[]\n  /**\n   * Filter by user ID (playlist creator’s ID)\n   */\n  userId?: string\n  /**\n   * Filter by track ID – only playlists containing this track will be returned\n   */\n  trackId?: string\n}\n\nexport type PlaylistsPublicControllerGetPlaylistsSortBy =\n  (typeof PlaylistsPublicControllerGetPlaylistsSortBy)[keyof typeof PlaylistsPublicControllerGetPlaylistsSortBy]\n\nexport const PlaylistsPublicControllerGetPlaylistsSortBy = {\n  addedAt: 'addedAt',\n  likesCount: 'likesCount',\n} as const\n\nexport type PlaylistsPublicControllerGetPlaylistsSortDirection =\n  (typeof PlaylistsPublicControllerGetPlaylistsSortDirection)[keyof typeof PlaylistsPublicControllerGetPlaylistsSortDirection]\n\nexport const PlaylistsPublicControllerGetPlaylistsSortDirection = {\n  asc: 'asc',\n  desc: 'desc',\n} as const\n\nexport type PlaylistsControllerUploadMainImageBody = {\n  /**\n   * Maximum size 1 MB; minimum height 500px; image must be square\n   * @maxLength 1048576\n   */\n  file: BinaryFile\n}\n\nexport type TracksPublicControllerGetAllTracksParams = {\n  /**\n   * Page number for pagination (starting from 1)\n   * @minimum 1\n   */\n  pageNumber?: number\n  /**\n   * Page size for pagination (between 1 and 20)\n   * @minimum 1\n   * @maximum 20\n   */\n  pageSize?: number\n  /**\n   * Search term for filtering playlists by name\n   */\n  search?: string\n  /**\n   * Field by which to sort tracks\n   */\n  sortBy?: TracksPublicControllerGetAllTracksSortBy\n  /**\n   * Sort direction (ascending or descending)\n   */\n  sortDirection?: TracksPublicControllerGetAllTracksSortDirection\n  /**\n   * Filter by tag IDs (multiple values allowed)\n   */\n  tagsIds?: string[]\n  /**\n   * Filter by artist IDs (multiple values allowed)\n   */\n  artistsIds?: string[]\n  /**\n   * Filter by user ID (track creator's ID)\n   */\n  userId?: string\n  /**\n   * If true, include unpublished tracks (drafts) of current user if userId === currentUserId\n   */\n  includeDrafts?: boolean\n  /**\n   * Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination.\n   */\n  paginationType?: TracksPublicControllerGetAllTracksPaginationType\n  /**\n   * Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\".\n   * @nullable\n   */\n  cursor?: string | null\n}\n\nexport type TracksPublicControllerGetAllTracksSortBy =\n  (typeof TracksPublicControllerGetAllTracksSortBy)[keyof typeof TracksPublicControllerGetAllTracksSortBy]\n\nexport const TracksPublicControllerGetAllTracksSortBy = {\n  publishedAt: 'publishedAt',\n  likesCount: 'likesCount',\n} as const\n\nexport type TracksPublicControllerGetAllTracksSortDirection =\n  (typeof TracksPublicControllerGetAllTracksSortDirection)[keyof typeof TracksPublicControllerGetAllTracksSortDirection]\n\nexport const TracksPublicControllerGetAllTracksSortDirection = {\n  asc: 'asc',\n  desc: 'desc',\n} as const\n\nexport type TracksPublicControllerGetAllTracksPaginationType =\n  (typeof TracksPublicControllerGetAllTracksPaginationType)[keyof typeof TracksPublicControllerGetAllTracksPaginationType]\n\nexport const TracksPublicControllerGetAllTracksPaginationType = {\n  offset: 'offset',\n  cursor: 'cursor',\n} as const\n\nexport type TracksControllerUploadTrackCoverBody = {\n  cover: Blob\n}\n\nexport type TracksControllerUploadTrackMp3Body = {\n  title: string\n  file: Blob\n}\n\nexport type ArtistsControllerSearchArtistParams = {\n  search: string\n}\n\nexport type AuthControllerOauthRedirectParams = {\n  /**\n * The callback URL to redirect after grand access,\n     https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\n */\n  callbackUrl: string\n}\n\nexport type TagsControllerSearchTagsParams = {\n  /**\n   * Substring to search tags by (using normalized name)\n   */\n  search: string\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/musicfun.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nimport { customInstance } from './custom-instance'\nexport interface UserOutputDTO {\n  /** Unique identifier of the user */\n  id: string\n  /** Name of the user */\n  name: string\n}\n\n/**\n * Type of the image size (e.g., original, thumbnail variants)\n */\nexport type ImageSizeType = (typeof ImageSizeType)[keyof typeof ImageSizeType]\n\nexport const ImageSizeType = {\n  original: 'original',\n  thumbnail: 'thumbnail',\n  medium: 'medium',\n} as const\n\nexport interface ImageDto {\n  /** Type of the image size (e.g., original, thumbnail variants) */\n  type: ImageSizeType\n  /** Image width in pixels */\n  width: number\n  /** Image height in pixels */\n  height: number\n  /** Image file size in bytes */\n  fileSize: number\n  /** Full public URL of the image */\n  url: string\n}\n\nexport interface PlaylistImagesOutputDTO {\n  /** Original images and thumbnail previews */\n  main?: ImageDto[]\n}\n\nexport interface GetTagOutput {\n  /** Unique identifier of the tag */\n  id: string\n  /** Original name of the tag */\n  name: string\n}\n\n/**\n * User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike\n */\nexport type ReactionValue = (typeof ReactionValue)[keyof typeof ReactionValue]\n\nexport const ReactionValue = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface PlaylistAttributesDto {\n  /** Title of the playlist */\n  title: string\n  /**\n   * Description of the playlist\n   * @nullable\n   */\n  description: string | null\n  /** Date and time when the playlist was added (ISO 8601) */\n  addedAt: string\n  /** Date and time when the playlist was last updated (ISO 8601) */\n  updatedAt: string\n  /** Order index of the playlist */\n  order: number\n  /** User who created the playlist */\n  user: UserOutputDTO\n  /** Images associated with the playlist */\n  images: PlaylistImagesOutputDTO\n  /** Tags linked to the playlist */\n  tags: GetTagOutput[]\n  /** Total number of likes for this playlist */\n  likesCount: number\n  /** Total number of dislikes for this playlist */\n  dislikesCount: number\n  /** User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n  currentUserReaction: ReactionValue\n}\n\nexport interface PlaylistListItemJsonApiData {\n  /** Unique identifier of the playlist */\n  id: string\n  /** Resource type (should be \"playlists\") */\n  type: string\n  /** Attributes of the playlist resource */\n  attributes: PlaylistAttributesDto\n}\n\nexport interface GetMyPlaylistsOutput {\n  /** Array of playlist resource objects owned by the current user */\n  data: PlaylistListItemJsonApiData[]\n}\n\nexport interface CreatePlaylistRequestPayload {\n  /**\n   * Playlist title (1 to 100 characters)\n   * @minLength 1\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Playlist description (up to 1000 characters)\n   * @maxLength 1000\n   * @nullable\n   */\n  description: string | null\n}\n\nexport interface PlaylistOutputAttributes {\n  /** Title of the playlist */\n  title: string\n  /**\n   * Description of the playlist\n   * @nullable\n   */\n  description: string | null\n  /** Date and time when the playlist was added (ISO 8601) */\n  addedAt: string\n  /** Date and time when the playlist was last updated (ISO 8601) */\n  updatedAt: string\n  /** Order index of the playlist */\n  order: number\n  /** User who created the playlist */\n  user: UserOutputDTO\n  /** Images associated with the playlist */\n  images: PlaylistImagesOutputDTO\n  /** Tags linked to the playlist */\n  tags: GetTagOutput[]\n  /** Total number of likes for this playlist */\n  likesCount: number\n  /** Total number of dislikes for this playlist */\n  dislikesCount: number\n  /** User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n  currentUserReaction: ReactionValue\n}\n\nexport interface PlaylistOutput {\n  /** Unique identifier of the playlist */\n  id: string\n  /** Resource type (should be \"playlists\") */\n  type: string\n  /** Playlist attributes object */\n  attributes: PlaylistOutputAttributes\n}\n\nexport interface GetPlaylistOutput {\n  /** JSON:API single-resource response wrapper */\n  data: PlaylistOutput\n}\n\nexport interface UpdatePlaylistRequestPayload {\n  /**\n   * Playlist title (1 – 100 characters)\n   * @minLength 1\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Playlist description (up to 1000 characters)\n   * @maxLength 1000\n   * @nullable\n   */\n  description: string | null\n  /**\n   * Tag IDs to associate with the playlist (0 – 5 items; [] = clear tags)\n   * @maxItems 5\n   */\n  tagIds: string[]\n}\n\nexport interface ReorderPlaylistsRequestPayload {\n  /**\n   * ID of the playlist after which the current playlist should be inserted. Send null to place the playlist at the beginning of the list.\n   * @nullable\n   */\n  putAfterItemId: string | null\n}\n\nexport interface GetImagesOutput {\n  /** List of original images and thumbnail versions (e.g., original, 320x180, etc.) */\n  main?: ImageDto[]\n}\n\n/**\n * Field by which to sort tracks\n */\nexport type GetTracksRequestPayloadSortBy =\n  (typeof GetTracksRequestPayloadSortBy)[keyof typeof GetTracksRequestPayloadSortBy]\n\nexport const GetTracksRequestPayloadSortBy = {\n  publishedAt: 'publishedAt',\n  likesCount: 'likesCount',\n} as const\n\n/**\n * Sort direction (ascending or descending)\n */\nexport type GetTracksRequestPayloadSortDirection =\n  (typeof GetTracksRequestPayloadSortDirection)[keyof typeof GetTracksRequestPayloadSortDirection]\n\nexport const GetTracksRequestPayloadSortDirection = {\n  asc: 'asc',\n  desc: 'desc',\n} as const\n\n/**\n * Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination.\n */\nexport type GetTracksRequestPayloadPaginationType =\n  (typeof GetTracksRequestPayloadPaginationType)[keyof typeof GetTracksRequestPayloadPaginationType]\n\nexport const GetTracksRequestPayloadPaginationType = {\n  offset: 'offset',\n  cursor: 'cursor',\n} as const\n\nexport interface GetTracksRequestPayload {\n  /**\n   * Page number for pagination (starting from 1)\n   * @minimum 1\n   */\n  pageNumber?: number\n  /**\n   * Page size for pagination (between 1 and 20)\n   * @minimum 1\n   * @maximum 20\n   */\n  pageSize?: number\n  /** Search term for filtering playlists by name */\n  search?: string\n  /** Field by which to sort tracks */\n  sortBy?: GetTracksRequestPayloadSortBy\n  /** Sort direction (ascending or descending) */\n  sortDirection?: GetTracksRequestPayloadSortDirection\n  /** Filter by tag IDs (multiple values allowed) */\n  tagsIds?: string[]\n  /** Filter by artist IDs (multiple values allowed) */\n  artistsIds?: string[]\n  /** Filter by user ID (track creator's ID) */\n  userId?: string\n  /** If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n  includeDrafts?: boolean\n  /** Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination. */\n  paginationType?: GetTracksRequestPayloadPaginationType\n  /**\n   * Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\".\n   * @nullable\n   */\n  cursor?: string | null\n}\n\nexport interface JsonApiErrorSource {\n  /** e.g. \"/data/attributes/field\" */\n  pointer?: string\n  /** e.g. \"?queryParam\" */\n  parameter?: string\n}\n\n/**\n * Application-specific error code\n */\nexport type JsonApiErrorCode = { [key: string]: unknown }\n\n/**\n * Any extra data\n */\nexport type JsonApiErrorMeta = { [key: string]: unknown }\n\nexport interface JsonApiError {\n  /** HTTP status code as a string */\n  status: string\n  /** Application-specific error code */\n  code?: JsonApiErrorCode\n  /** Short, human-readable summary */\n  title?: string\n  /** Detailed explanation */\n  detail?: string\n  /** Pointer to the associated entity in the request */\n  source?: JsonApiErrorSource\n  /** Any extra data */\n  meta?: JsonApiErrorMeta\n}\n\n/**\n * e.g. timestamp, path, traceId, etc.\n */\nexport type JsonApiErrorDocumentMeta = { [key: string]: unknown }\n\nexport interface JsonApiErrorDocument {\n  /** Array of one or more errors */\n  errors: JsonApiError[]\n  /** e.g. timestamp, path, traceId, etc. */\n  meta?: JsonApiErrorDocumentMeta\n}\n\nexport interface AttachmentDto {\n  /** Unique identifier of the entity */\n  id: string\n  /** Date and time when the entity was added */\n  addedAt: string\n  /** Date and time when the entity was last updated */\n  updatedAt: string\n  /** Version number of the entity (for concurrency control) */\n  version: number\n  /** Public URL to access the uploaded file */\n  url: string\n  /** MIME type of the file */\n  contentType: string\n  /** Original filename uploaded by the user */\n  originalName: string\n  /** Size of the file in bytes */\n  fileSize: number\n}\n\n/**\n * 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк\n */\nexport type TrackListItemOutputAttributesCurrentUserReaction =\n  (typeof TrackListItemOutputAttributesCurrentUserReaction)[keyof typeof TrackListItemOutputAttributesCurrentUserReaction]\n\nexport const TrackListItemOutputAttributesCurrentUserReaction = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface TrackListItemOutputAttributes {\n  title: string\n  addedAt: string\n  likesCount: number\n  attachments: AttachmentDto[]\n  images: GetImagesOutput\n  user: UserOutputDTO\n  /** 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк */\n  currentUserReaction: TrackListItemOutputAttributesCurrentUserReaction\n  isPublished: boolean\n  publishedAt?: string\n}\n\nexport interface ArtistRelationship {\n  id: string\n  type: string\n}\n\nexport interface ArtistsRelationship {\n  data: ArtistRelationship[]\n}\n\nexport interface TrackRelationships {\n  artists: ArtistsRelationship\n}\n\nexport interface TrackListItemOutput {\n  id: string\n  type: string\n  attributes: TrackListItemOutputAttributes\n  relationships: TrackRelationships\n}\n\nexport interface JsonApiMetaWithPagingAndCursor {\n  page: number\n  pageSize: number\n  /**\n   * Total count may be absent when using keyset pagination\n   * @nullable\n   */\n  totalCount: number | null\n  /**\n   * Total number of pages\n   * @nullable\n   */\n  pagesCount: number | null\n  /**\n   * Cursor for the next page\n   * @nullable\n   */\n  nextCursor: string | null\n}\n\nexport interface OmitTypeClass {\n  /** Name of the artist */\n  name: string\n}\n\nexport interface IncludedArtistOutput {\n  id: string\n  type: string\n  attributes: OmitTypeClass\n}\n\nexport interface GetTrackListOutput {\n  data: TrackListItemOutput[]\n  meta: JsonApiMetaWithPagingAndCursor\n  included: IncludedArtistOutput[]\n}\n\n/**\n * User reaction: 0 – guest or no reaction; 1 – liked; -1 – disliked\n * @nullable\n */\nexport type PlaylistTrackAttributesCurrentUserReaction =\n  | (typeof PlaylistTrackAttributesCurrentUserReaction)[keyof typeof PlaylistTrackAttributesCurrentUserReaction]\n  | null\n\nexport const PlaylistTrackAttributesCurrentUserReaction = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface PlaylistTrackAttributes {\n  /** Title of the track */\n  title: string\n  /** Order index of the track in the playlist */\n  order: number\n  /** Date and time when the track was added to the playlist (ISO 8601) */\n  addedAt: string\n  /** Date and time when the track was last updated in the playlist (ISO 8601) */\n  updatedAt: string\n  /** Attachments related to the track */\n  attachments: AttachmentDto[]\n  /** Images associated with the track */\n  images: GetImagesOutput\n  /**\n   * User reaction: 0 – guest or no reaction; 1 – liked; -1 – disliked\n   * @nullable\n   */\n  currentUserReaction: PlaylistTrackAttributesCurrentUserReaction\n}\n\nexport interface GetPlaylistTrackListOutputData {\n  id: string\n  type: string\n  attributes: PlaylistTrackAttributes\n  relationships: TrackRelationships\n}\n\nexport interface JsonApiMeta {\n  totalCount: number\n}\n\nexport interface GetPlaylistTrackListOutput {\n  data: GetPlaylistTrackListOutputData[]\n  meta: JsonApiMeta\n  included: IncludedArtistOutput[]\n}\n\nexport interface GetArtistOutput {\n  /** Unique identifier of the artist */\n  id: string\n  /** Name of the artist */\n  name: string\n}\n\n/**\n * User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n */\nexport type TrackDetailsAttributesCurrentUserReaction =\n  (typeof TrackDetailsAttributesCurrentUserReaction)[keyof typeof TrackDetailsAttributesCurrentUserReaction]\n\nexport const TrackDetailsAttributesCurrentUserReaction = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface TrackDetailsAttributes {\n  /**\n   * Track title\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Track lyrics text\n   * @maxLength 5000\n   * @nullable\n   */\n  lyrics?: string | null\n  /**\n   * Release date in ISO 8601 format\n   * @nullable\n   */\n  releaseDate?: string | null\n  /** Date and time when the track was added (ISO 8601) */\n  addedAt: string\n  /** Date and time when the track was last updated (ISO 8601) */\n  updatedAt: string\n  /** Duration of the track in seconds */\n  duration: number\n  /** Total number of likes for this track */\n  likesCount: number\n  /**\n   * Total number of dislikes for this track\n   * @deprecated\n   */\n  dislikesCount: number\n  /** List of attachments related to the track */\n  attachments: AttachmentDto[]\n  /** Images associated with the track */\n  images: GetImagesOutput\n  /** Tags associated with the track */\n  tags: GetTagOutput[]\n  /** Artists associated with the track */\n  artists: GetArtistOutput[]\n  user: UserOutputDTO\n  /** Publication status of the track */\n  isPublished: boolean\n  /**\n   * Publication date in ISO 8601 format\n   * @nullable\n   */\n  publishedAt?: string | null\n  /** User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked */\n  currentUserReaction: TrackDetailsAttributesCurrentUserReaction\n}\n\nexport interface TrackDetailsData {\n  /** Unique identifier of the track */\n  id: string\n  /** Resource type (should be \"tracks\") */\n  type: string\n  /** Detailed attributes of the track resource */\n  attributes: TrackDetailsAttributes\n}\n\nexport interface GetTrackDetailsOutput {\n  /** JSON:API single-track details response wrapper */\n  data: TrackDetailsData\n}\n\nexport type ReactionOutputValue = (typeof ReactionOutputValue)[keyof typeof ReactionOutputValue]\n\nexport const ReactionOutputValue = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface ReactionOutput {\n  objectId: string\n  value: ReactionOutputValue\n  likes: number\n  dislikes: number\n}\n\n/**\n * Field by which to sort playlists\n */\nexport type GetPlaylistsRequestPayloadSortBy =\n  (typeof GetPlaylistsRequestPayloadSortBy)[keyof typeof GetPlaylistsRequestPayloadSortBy]\n\nexport const GetPlaylistsRequestPayloadSortBy = {\n  addedAt: 'addedAt',\n  likesCount: 'likesCount',\n} as const\n\n/**\n * Sort direction (ascending or descending)\n */\nexport type GetPlaylistsRequestPayloadSortDirection =\n  (typeof GetPlaylistsRequestPayloadSortDirection)[keyof typeof GetPlaylistsRequestPayloadSortDirection]\n\nexport const GetPlaylistsRequestPayloadSortDirection = {\n  asc: 'asc',\n  desc: 'desc',\n} as const\n\nexport interface GetPlaylistsRequestPayload {\n  /**\n   * Page number for pagination (starting from 1)\n   * @minimum 1\n   */\n  pageNumber?: number\n  /**\n   * Page size for pagination (between 1 and 20)\n   * @minimum 1\n   * @maximum 20\n   */\n  pageSize?: number\n  /** Search term for filtering playlists by name */\n  search?: string\n  /** Field by which to sort playlists */\n  sortBy?: GetPlaylistsRequestPayloadSortBy\n  /** Sort direction (ascending or descending) */\n  sortDirection?: GetPlaylistsRequestPayloadSortDirection\n  /** Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n  tagsIds?: string[]\n  /** Filter by user ID (playlist creator’s ID) */\n  userId?: string\n  /** Filter by track ID – only playlists containing this track will be returned */\n  trackId?: string\n}\n\nexport interface JsonApiMetaWithPaging {\n  totalCount: number\n  page: number\n  pageSize: number\n  pagesCount: number\n}\n\nexport interface GetPlaylistsOutput {\n  /** Array of playlist resource objects */\n  data: PlaylistListItemJsonApiData[]\n  /** Pagination metadata for the playlists list */\n  meta: JsonApiMetaWithPaging\n}\n\nexport interface ReorderTracksRequestPayload {\n  /**\n   * ID of the track after which the current track should be inserted. Send null to place the track at the beginning of the list.\n   * @nullable\n   */\n  putAfterItemId: string | null\n}\n\nexport interface UpdateTrackRequestPayload {\n  /**\n   * Track title (1 to 100 characters)\n   * @minLength 1\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Track lyrics (up to 5000 characters)\n   * @maxLength 5000\n   * @nullable\n   */\n  lyrics: string | null\n  /**\n   * Release date in ISO 8601 format\n   * @nullable\n   */\n  releaseDate: string | null\n  /**\n   * Array of tag IDs to associate with the track (up to 5)\n   * @maxItems 5\n   */\n  tagIds: string[]\n  /**\n   * Array of artist IDs to associate with the track (up to 5)\n   * @maxItems 5\n   */\n  artistsIds: string[]\n}\n\n/**\n * User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n */\nexport type TrackOutputAttributesCurrentUserReaction =\n  (typeof TrackOutputAttributesCurrentUserReaction)[keyof typeof TrackOutputAttributesCurrentUserReaction]\n\nexport const TrackOutputAttributesCurrentUserReaction = {\n  NUMBER_0: 0,\n  NUMBER_1: 1,\n  NUMBER_MINUS_1: -1,\n} as const\n\nexport interface TrackOutputAttributes {\n  /**\n   * Track title\n   * @maxLength 100\n   */\n  title: string\n  /**\n   * Track lyrics text\n   * @maxLength 5000\n   * @nullable\n   */\n  lyrics?: string | null\n  /**\n   * Release date in ISO 8601 format\n   * @nullable\n   */\n  releaseDate?: string | null\n  /** Date and time when the track was added (ISO 8601) */\n  addedAt: string\n  /** Date and time when the track was last updated (ISO 8601) */\n  updatedAt: string\n  /** Duration of the track in seconds */\n  duration: number\n  /** Total number of likes for this track */\n  likesCount: number\n  /**\n   * Total number of dislikes for this track\n   * @deprecated\n   */\n  dislikesCount: number\n  /** List of attachments related to the track */\n  attachments: AttachmentDto[]\n  /** Images associated with the track */\n  images: GetImagesOutput\n  /** Tags associated with the track */\n  tags: GetTagOutput[]\n  /** Artists associated with the track */\n  artists: GetArtistOutput[]\n  user: UserOutputDTO\n  /** Publication status of the track */\n  isPublished: boolean\n  /**\n   * Publication date in ISO 8601 format\n   * @nullable\n   */\n  publishedAt?: string | null\n  /** User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked */\n  currentUserReaction: TrackOutputAttributesCurrentUserReaction\n}\n\nexport interface TrackOutput {\n  /** Unique identifier of the track */\n  id: string\n  /** Resource type (should be \"tracks\") */\n  type: string\n  /** Attributes of the track resource */\n  attributes: TrackOutputAttributes\n}\n\nexport interface GetTrackOutput {\n  /** JSON:API single-track response wrapper */\n  data: TrackOutput\n}\n\nexport interface AddTrackToPlaylistRequestPayload {\n  /** ID of the track to add to the playlist */\n  trackId: string\n}\n\nexport interface CreateArtistRequestPayload {\n  /**\n   * Artist name (must be between 2 and 30 characters)\n   * @minLength 2\n   * @maxLength 30\n   */\n  name: string\n}\n\nexport interface LoginRequestPayload {\n  /** Authorization code received from OAuth server after redirect */\n  code: string\n  /** Specify the same redirect URI used in the initial OAuth server request */\n  redirectUri: string\n  /** Access token lifetime (default \"3m\"); must be a string like \"60s\", \"3m\", \"2h\", or \"1d\" */\n  accessTokenTTL?: string\n  /** Refresh token lifetime: if true, 30 days; if false, 30 minutes. accessTokenTTL must not exceed the refresh token lifetime */\n  rememberMe: boolean\n}\n\nexport interface RefreshOutput {\n  refreshToken: string\n  accessToken: string\n}\n\nexport interface BadRequestException {\n  [key: string]: unknown\n}\n\nexport interface UnauthorizedException {\n  [key: string]: unknown\n}\n\nexport interface RefreshRequestPayload {\n  refreshToken: string\n}\n\nexport interface LogoutRequestPayload {\n  refreshToken: string\n}\n\nexport interface GetMeOutput {\n  userId: string\n  login: string\n}\n\nexport interface CreateTagRequestPayload {\n  /**\n   * Tag name (2 to 30 characters)\n   * @minLength 2\n   * @maxLength 30\n   */\n  name: string\n}\n\n/**\n * Файл в multipart/form-data\n */\nexport type BinaryFile = Blob\n\nexport type PlaylistsPublicControllerGetPlaylistsParams = {\n  /**\n   * Page number for pagination (starting from 1)\n   * @minimum 1\n   */\n  pageNumber?: number\n  /**\n   * Page size for pagination (between 1 and 20)\n   * @minimum 1\n   * @maximum 20\n   */\n  pageSize?: number\n  /**\n   * Search term for filtering playlists by name\n   */\n  search?: string\n  /**\n   * Field by which to sort playlists\n   */\n  sortBy?: PlaylistsPublicControllerGetPlaylistsSortBy\n  /**\n   * Sort direction (ascending or descending)\n   */\n  sortDirection?: PlaylistsPublicControllerGetPlaylistsSortDirection\n  /**\n   * Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2\n   */\n  tagsIds?: string[]\n  /**\n   * Filter by user ID (playlist creator’s ID)\n   */\n  userId?: string\n  /**\n   * Filter by track ID – only playlists containing this track will be returned\n   */\n  trackId?: string\n}\n\nexport type PlaylistsPublicControllerGetPlaylistsSortBy =\n  (typeof PlaylistsPublicControllerGetPlaylistsSortBy)[keyof typeof PlaylistsPublicControllerGetPlaylistsSortBy]\n\nexport const PlaylistsPublicControllerGetPlaylistsSortBy = {\n  addedAt: 'addedAt',\n  likesCount: 'likesCount',\n} as const\n\nexport type PlaylistsPublicControllerGetPlaylistsSortDirection =\n  (typeof PlaylistsPublicControllerGetPlaylistsSortDirection)[keyof typeof PlaylistsPublicControllerGetPlaylistsSortDirection]\n\nexport const PlaylistsPublicControllerGetPlaylistsSortDirection = {\n  asc: 'asc',\n  desc: 'desc',\n} as const\n\nexport type PlaylistsControllerUploadMainImageBody = {\n  /**\n   * Maximum size 1 MB; minimum height 500px; image must be square\n   * @maxLength 1048576\n   */\n  file: BinaryFile\n}\n\nexport type TracksPublicControllerGetAllTracksParams = {\n  /**\n   * Page number for pagination (starting from 1)\n   * @minimum 1\n   */\n  pageNumber?: number\n  /**\n   * Page size for pagination (between 1 and 20)\n   * @minimum 1\n   * @maximum 20\n   */\n  pageSize?: number\n  /**\n   * Search term for filtering playlists by name\n   */\n  search?: string\n  /**\n   * Field by which to sort tracks\n   */\n  sortBy?: TracksPublicControllerGetAllTracksSortBy\n  /**\n   * Sort direction (ascending or descending)\n   */\n  sortDirection?: TracksPublicControllerGetAllTracksSortDirection\n  /**\n   * Filter by tag IDs (multiple values allowed)\n   */\n  tagsIds?: string[]\n  /**\n   * Filter by artist IDs (multiple values allowed)\n   */\n  artistsIds?: string[]\n  /**\n   * Filter by user ID (track creator's ID)\n   */\n  userId?: string\n  /**\n   * If true, include unpublished tracks (drafts) of current user if userId === currentUserId\n   */\n  includeDrafts?: boolean\n  /**\n   * Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination.\n   */\n  paginationType?: TracksPublicControllerGetAllTracksPaginationType\n  /**\n   * Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\".\n   * @nullable\n   */\n  cursor?: string | null\n}\n\nexport type TracksPublicControllerGetAllTracksSortBy =\n  (typeof TracksPublicControllerGetAllTracksSortBy)[keyof typeof TracksPublicControllerGetAllTracksSortBy]\n\nexport const TracksPublicControllerGetAllTracksSortBy = {\n  publishedAt: 'publishedAt',\n  likesCount: 'likesCount',\n} as const\n\nexport type TracksPublicControllerGetAllTracksSortDirection =\n  (typeof TracksPublicControllerGetAllTracksSortDirection)[keyof typeof TracksPublicControllerGetAllTracksSortDirection]\n\nexport const TracksPublicControllerGetAllTracksSortDirection = {\n  asc: 'asc',\n  desc: 'desc',\n} as const\n\nexport type TracksPublicControllerGetAllTracksPaginationType =\n  (typeof TracksPublicControllerGetAllTracksPaginationType)[keyof typeof TracksPublicControllerGetAllTracksPaginationType]\n\nexport const TracksPublicControllerGetAllTracksPaginationType = {\n  offset: 'offset',\n  cursor: 'cursor',\n} as const\n\nexport type TracksControllerUploadTrackCoverBody = {\n  cover: Blob\n}\n\nexport type TracksControllerUploadTrackMp3Body = {\n  title: string\n  file: Blob\n}\n\nexport type ArtistsControllerSearchArtistParams = {\n  search: string\n}\n\nexport type AuthControllerOauthRedirectParams = {\n  /**\n * The callback URL to redirect after grand access,\n     https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\n */\n  callbackUrl: string\n}\n\nexport type TagsControllerSearchTagsParams = {\n  /**\n   * Substring to search tags by (using normalized name)\n   */\n  search: string\n}\n\ntype SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]\n\n/**\n * @deprecated\n * @summary Get my playlists\n */\nexport const playlistsControllerGetMyPlaylists = (\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetMyPlaylistsOutput>({ url: `/playlists/my`, method: 'GET' }, options)\n}\n\n/**\n * @summary Create a new playlist\n */\nexport const playlistsControllerCreatePlaylist = (\n  createPlaylistRequestPayload: CreatePlaylistRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetPlaylistOutput>(\n    {\n      url: `/playlists`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: createPlaylistRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * Query parameters must conform to the **GetPlaylistsRequestPayload** schema.\n * @summary Retrieve all playlists\n */\nexport const playlistsPublicControllerGetPlaylists = (\n  params?: PlaylistsPublicControllerGetPlaylistsParams,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetPlaylistsOutput>({ url: `/playlists`, method: 'GET', params }, options)\n}\n\n/**\n * @summary Update a playlist\n */\nexport const playlistsControllerUpdatePlaylist = (\n  playlistId: string,\n  updatePlaylistRequestPayload: UpdatePlaylistRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    {\n      url: `/playlists/${playlistId}`,\n      method: 'PUT',\n      headers: { 'Content-Type': 'application/json' },\n      data: updatePlaylistRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Delete a playlist\n */\nexport const playlistsControllerDeletePlaylist = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/playlists/${playlistId}`, method: 'DELETE' }, options)\n}\n\n/**\n * @summary Get a single playlist by ID\n */\nexport const playlistsPublicControllerGetPlaylistById = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetPlaylistOutput>(\n    { url: `/playlists/${playlistId}`, method: 'GET' },\n    options\n  )\n}\n\n/**\n * @summary Reorder playlists\n */\nexport const playlistsControllerReorderPlaylist = (\n  playlistId: string,\n  reorderPlaylistsRequestPayload: ReorderPlaylistsRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    {\n      url: `/playlists/${playlistId}/reorder`,\n      method: 'PUT',\n      headers: { 'Content-Type': 'application/json' },\n      data: reorderPlaylistsRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * Minimum height — 500px; image must be square\n * @summary Upload playlist cover\n */\nexport const playlistsControllerUploadMainImage = (\n  playlistId: string,\n  playlistsControllerUploadMainImageBody: PlaylistsControllerUploadMainImageBody,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  const formData = new FormData()\n  formData.append(`file`, playlistsControllerUploadMainImageBody.file)\n\n  return customInstance<GetImagesOutput>(\n    {\n      url: `/playlists/${playlistId}/images/main`,\n      method: 'POST',\n      headers: { 'Content-Type': 'multipart/form-data' },\n      data: formData,\n    },\n    options\n  )\n}\n\n/**\n * @summary Delete playlist cover\n */\nexport const playlistsControllerDeleteTrackCover = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    { url: `/playlists/${playlistId}/images/main`, method: 'DELETE' },\n    options\n  )\n}\n\n/**\n * @summary Get list of all tracks in all playlists\n */\nexport const tracksPublicControllerGetAllTracks = (\n  params?: TracksPublicControllerGetAllTracksParams,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetTrackListOutput>(\n    { url: `/playlists/tracks`, method: 'GET', params },\n    options\n  )\n}\n\n/**\n * @summary Get list of tracks in a playlist\n */\nexport const tracksPublicControllerGetPlaylistTracks = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetPlaylistTrackListOutput>(\n    { url: `/playlists/${playlistId}/tracks`, method: 'GET' },\n    options\n  )\n}\n\n/**\n * @summary Get track details by ID\n */\nexport const tracksPublicControllerGetTrackDetails = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetTrackDetailsOutput>(\n    { url: `/playlists/tracks/${trackId}`, method: 'GET' },\n    options\n  )\n}\n\n/**\n * @summary Update track information\n */\nexport const tracksControllerUpdateTrack = (\n  trackId: string,\n  updateTrackRequestPayload: UpdateTrackRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetTrackOutput>(\n    {\n      url: `/playlists/tracks/${trackId}`,\n      method: 'PUT',\n      headers: { 'Content-Type': 'application/json' },\n      data: updateTrackRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Permanently delete a track\n */\nexport const tracksControllerDeleteTrackCompletely = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/playlists/tracks/${trackId}`, method: 'DELETE' }, options)\n}\n\n/**\n * @summary Like or toggle like on a track\n */\nexport const tracksPublicControllerLikeTrack = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/tracks/${trackId}/likes`, method: 'POST' },\n    options\n  )\n}\n\n/**\n * @summary Dislike or toggle dislike on a track\n */\nexport const tracksPublicControllerDislikeTrack = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/tracks/${trackId}/dislikes`, method: 'POST' },\n    options\n  )\n}\n\n/**\n * @summary Remove user reaction from a track\n */\nexport const tracksPublicControllerRemoveTrackReaction = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/tracks/${trackId}/reactions`, method: 'DELETE' },\n    options\n  )\n}\n\n/**\n * @summary Like a playlist\n */\nexport const playlistsPublicControllerLikePlaylist = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/${playlistId}/likes`, method: 'POST' },\n    options\n  )\n}\n\n/**\n * @summary Dislike a playlist\n */\nexport const playlistsPublicControllerDislikePlaylist = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/${playlistId}/dislikes`, method: 'POST' },\n    options\n  )\n}\n\n/**\n * @summary Remove user reaction from a playlist\n */\nexport const playlistsPublicControllerRemovePlaylistReaction = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/${playlistId}/reactions`, method: 'DELETE' },\n    options\n  )\n}\n\n/**\n * @summary Reorder tracks in a playlist\n */\nexport const tracksControllerReorderTrack = (\n  playlistId: string,\n  trackId: string,\n  reorderTracksRequestPayload: ReorderTracksRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    {\n      url: `/playlists/${playlistId}/tracks/${trackId}/reorder`,\n      method: 'PUT',\n      headers: { 'Content-Type': 'application/json' },\n      data: reorderTracksRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Add a track to your playlist\n */\nexport const tracksControllerAddTrackToPlaylist = (\n  playlistId: string,\n  addTrackToPlaylistRequestPayload: AddTrackToPlaylistRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    {\n      url: `/playlists/${playlistId}/relationships/tracks`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: addTrackToPlaylistRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Remove a track from your playlist\n */\nexport const tracksControllerUnbindTrackFromPlaylist = (\n  playlistId: string,\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    { url: `/playlists/${playlistId}/relationships/tracks/${trackId}`, method: 'DELETE' },\n    options\n  )\n}\n\n/**\n * @summary Publish a track (make it publicly available)\n */\nexport const tracksControllerPublishTrack = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    { url: `/playlists/tracks/${trackId}/actions/publish`, method: 'POST' },\n    options\n  )\n}\n\n/**\n * @summary Upload track cover\n */\nexport const tracksControllerUploadTrackCover = (\n  trackId: string,\n  tracksControllerUploadTrackCoverBody: TracksControllerUploadTrackCoverBody,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  const formData = new FormData()\n  formData.append(`cover`, tracksControllerUploadTrackCoverBody.cover)\n\n  return customInstance<GetImagesOutput>(\n    {\n      url: `/playlists/tracks/${trackId}/cover`,\n      method: 'POST',\n      headers: { 'Content-Type': 'multipart/form-data' },\n      data: formData,\n    },\n    options\n  )\n}\n\n/**\n * @summary Delete track cover\n */\nexport const tracksControllerDeleteTrackCover = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    { url: `/playlists/tracks/${trackId}/cover`, method: 'DELETE' },\n    options\n  )\n}\n\n/**\n * @summary Create a track with MP3 file upload\n */\nexport const tracksControllerUploadTrackMp3 = (\n  tracksControllerUploadTrackMp3Body: TracksControllerUploadTrackMp3Body,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  const formData = new FormData()\n  formData.append(`title`, tracksControllerUploadTrackMp3Body.title)\n  formData.append(`file`, tracksControllerUploadTrackMp3Body.file)\n\n  return customInstance<GetTrackOutput>(\n    {\n      url: `/playlists/tracks/upload`,\n      method: 'POST',\n      headers: { 'Content-Type': 'multipart/form-data' },\n      data: formData,\n    },\n    options\n  )\n}\n\n/**\n * @summary Create a new artist\n */\nexport const artistsControllerCreateArtist = (\n  createArtistRequestPayload: CreateArtistRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetArtistOutput>(\n    {\n      url: `/artists`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: createArtistRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Search artists by substring\n */\nexport const artistsControllerSearchArtist = (\n  params: ArtistsControllerSearchArtistParams,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetArtistOutput[]>(\n    { url: `/artists/search`, method: 'GET', params },\n    options\n  )\n}\n\n/**\n * @summary Delete an artist by ID\n */\nexport const artistsControllerDeleteArtist = (\n  id: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/artists/${id}`, method: 'DELETE' }, options)\n}\n\n/**\n * The callback URL to redirect after granting access, <a target=\"_blank\" href=\"https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\">https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=musicfun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid</a>\n * @summary OAuth redirect\n */\nexport const authControllerOauthRedirect = (\n  params: AuthControllerOauthRedirectParams,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/auth/oauth-redirect`, method: 'GET', params }, options)\n}\n\n/**\n * @summary Log in using the code received after OAuth authorization redirect\n */\nexport const authControllerLogin = (\n  loginRequestPayload: LoginRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<RefreshOutput>(\n    {\n      url: `/auth/login`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: loginRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Refresh refresh/access token pair\n */\nexport const authControllerRefresh = (\n  refreshRequestPayload: RefreshRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<RefreshOutput>(\n    {\n      url: `/auth/refresh`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: refreshRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Deactivate refresh token\n */\nexport const authControllerLogout = (\n  logoutRequestPayload: LogoutRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    {\n      url: `/auth/logout`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: logoutRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Get current user by access token\n */\nexport const authControllerGetMe = (options?: SecondParameter<typeof customInstance>) => {\n  return customInstance<GetMeOutput>({ url: `/auth/me`, method: 'GET' }, options)\n}\n\n/**\n * @summary Create a new tag\n */\nexport const tagsControllerCreateTag = (\n  createTagRequestPayload: CreateTagRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetTagOutput>(\n    {\n      url: `/tags`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: createTagRequestPayload,\n    },\n    options\n  )\n}\n\n/**\n * @summary Search tags by substring\n */\nexport const tagsControllerSearchTags = (\n  params: TagsControllerSearchTagsParams,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetTagOutput[]>({ url: `/tags/search`, method: 'GET', params }, options)\n}\n\n/**\n * @summary Delete a tag by ID\n */\nexport const tagsControllerDeleteTag = (\n  id: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/tags/${id}`, method: 'DELETE' }, options)\n}\n\nexport type PlaylistsControllerGetMyPlaylistsResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>\n>\nexport type PlaylistsControllerCreatePlaylistResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerCreatePlaylist>>\n>\nexport type PlaylistsPublicControllerGetPlaylistsResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>\n>\nexport type PlaylistsControllerUpdatePlaylistResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerUpdatePlaylist>>\n>\nexport type PlaylistsControllerDeletePlaylistResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerDeletePlaylist>>\n>\nexport type PlaylistsPublicControllerGetPlaylistByIdResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>\n>\nexport type PlaylistsControllerReorderPlaylistResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerReorderPlaylist>>\n>\nexport type PlaylistsControllerUploadMainImageResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerUploadMainImage>>\n>\nexport type PlaylistsControllerDeleteTrackCoverResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerDeleteTrackCover>>\n>\nexport type TracksPublicControllerGetAllTracksResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>\n>\nexport type TracksPublicControllerGetPlaylistTracksResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>\n>\nexport type TracksPublicControllerGetTrackDetailsResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>\n>\nexport type TracksControllerUpdateTrackResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerUpdateTrack>>\n>\nexport type TracksControllerDeleteTrackCompletelyResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerDeleteTrackCompletely>>\n>\nexport type TracksPublicControllerLikeTrackResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerLikeTrack>>\n>\nexport type TracksPublicControllerDislikeTrackResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerDislikeTrack>>\n>\nexport type TracksPublicControllerRemoveTrackReactionResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerRemoveTrackReaction>>\n>\nexport type PlaylistsPublicControllerLikePlaylistResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerLikePlaylist>>\n>\nexport type PlaylistsPublicControllerDislikePlaylistResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerDislikePlaylist>>\n>\nexport type PlaylistsPublicControllerRemovePlaylistReactionResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerRemovePlaylistReaction>>\n>\nexport type TracksControllerReorderTrackResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerReorderTrack>>\n>\nexport type TracksControllerAddTrackToPlaylistResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerAddTrackToPlaylist>>\n>\nexport type TracksControllerUnbindTrackFromPlaylistResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerUnbindTrackFromPlaylist>>\n>\nexport type TracksControllerPublishTrackResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerPublishTrack>>\n>\nexport type TracksControllerUploadTrackCoverResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerUploadTrackCover>>\n>\nexport type TracksControllerDeleteTrackCoverResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerDeleteTrackCover>>\n>\nexport type TracksControllerUploadTrackMp3Result = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerUploadTrackMp3>>\n>\nexport type ArtistsControllerCreateArtistResult = NonNullable<\n  Awaited<ReturnType<typeof artistsControllerCreateArtist>>\n>\nexport type ArtistsControllerSearchArtistResult = NonNullable<\n  Awaited<ReturnType<typeof artistsControllerSearchArtist>>\n>\nexport type ArtistsControllerDeleteArtistResult = NonNullable<\n  Awaited<ReturnType<typeof artistsControllerDeleteArtist>>\n>\nexport type AuthControllerOauthRedirectResult = NonNullable<\n  Awaited<ReturnType<typeof authControllerOauthRedirect>>\n>\nexport type AuthControllerLoginResult = NonNullable<Awaited<ReturnType<typeof authControllerLogin>>>\nexport type AuthControllerRefreshResult = NonNullable<\n  Awaited<ReturnType<typeof authControllerRefresh>>\n>\nexport type AuthControllerLogoutResult = NonNullable<\n  Awaited<ReturnType<typeof authControllerLogout>>\n>\nexport type AuthControllerGetMeResult = NonNullable<Awaited<ReturnType<typeof authControllerGetMe>>>\nexport type TagsControllerCreateTagResult = NonNullable<\n  Awaited<ReturnType<typeof tagsControllerCreateTag>>\n>\nexport type TagsControllerSearchTagsResult = NonNullable<\n  Awaited<ReturnType<typeof tagsControllerSearchTags>>\n>\nexport type TagsControllerDeleteTagResult = NonNullable<\n  Awaited<ReturnType<typeof tagsControllerDeleteTag>>\n>\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/playlists-owner/playlists-owner.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nimport type {\n  DataTag,\n  DefinedInitialDataOptions,\n  DefinedUseQueryResult,\n  MutationFunction,\n  QueryClient,\n  QueryFunction,\n  QueryKey,\n  UndefinedInitialDataOptions,\n  UseMutationOptions,\n  UseMutationResult,\n  UseQueryOptions,\n  UseQueryResult,\n} from '@tanstack/react-query'\nimport { useMutation, useQuery } from '@tanstack/react-query'\n\nimport { customInstance } from '.././custom-instance'\nimport type {\n  CreatePlaylistRequestPayload,\n  GetImagesOutput,\n  GetMyPlaylistsOutput,\n  GetPlaylistOutput,\n  PlaylistsControllerUploadMainImageBody,\n  ReorderPlaylistsRequestPayload,\n  UpdatePlaylistRequestPayload,\n} from '../musicfun.schemas'\n\ntype SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]\n\n/**\n * @deprecated\n * @summary Get my playlists\n */\nexport const playlistsControllerGetMyPlaylists = (\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetMyPlaylistsOutput>(\n    { url: `/playlists/my`, method: 'GET', signal },\n    options\n  )\n}\n\nexport const getPlaylistsControllerGetMyPlaylistsQueryKey = () => {\n  return [`/playlists/my`] as const\n}\n\nexport const getPlaylistsControllerGetMyPlaylistsQueryOptions = <\n  TData = Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>,\n  TError = null,\n>(options?: {\n  query?: Partial<\n    UseQueryOptions<Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>, TError, TData>\n  >\n  request?: SecondParameter<typeof customInstance>\n}) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey = queryOptions?.queryKey ?? getPlaylistsControllerGetMyPlaylistsQueryKey()\n\n  const queryFn: QueryFunction<Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>> = ({\n    signal,\n  }) => playlistsControllerGetMyPlaylists(requestOptions, signal)\n\n  return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type PlaylistsControllerGetMyPlaylistsQueryResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>\n>\nexport type PlaylistsControllerGetMyPlaylistsQueryError = null\n\nexport function usePlaylistsControllerGetMyPlaylists<\n  TData = Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>,\n  TError = null,\n>(\n  options: {\n    query: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>, TError, TData>\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>,\n          TError,\n          Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function usePlaylistsControllerGetMyPlaylists<\n  TData = Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>,\n  TError = null,\n>(\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>, TError, TData>\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>,\n          TError,\n          Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function usePlaylistsControllerGetMyPlaylists<\n  TData = Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>,\n  TError = null,\n>(\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @deprecated\n * @summary Get my playlists\n */\n\nexport function usePlaylistsControllerGetMyPlaylists<\n  TData = Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>,\n  TError = null,\n>(\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof playlistsControllerGetMyPlaylists>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getPlaylistsControllerGetMyPlaylistsQueryOptions(options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Create a new playlist\n */\nexport const playlistsControllerCreatePlaylist = (\n  createPlaylistRequestPayload: CreatePlaylistRequestPayload,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetPlaylistOutput>(\n    {\n      url: `/playlists`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: createPlaylistRequestPayload,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getPlaylistsControllerCreatePlaylistMutationOptions = <\n  TError = null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsControllerCreatePlaylist>>,\n    TError,\n    { data: CreatePlaylistRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsControllerCreatePlaylist>>,\n  TError,\n  { data: CreatePlaylistRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['playlistsControllerCreatePlaylist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsControllerCreatePlaylist>>,\n    { data: CreatePlaylistRequestPayload }\n  > = (props) => {\n    const { data } = props ?? {}\n\n    return playlistsControllerCreatePlaylist(data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsControllerCreatePlaylistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerCreatePlaylist>>\n>\nexport type PlaylistsControllerCreatePlaylistMutationBody = CreatePlaylistRequestPayload\nexport type PlaylistsControllerCreatePlaylistMutationError = null\n\n/**\n * @summary Create a new playlist\n */\nexport const usePlaylistsControllerCreatePlaylist = <TError = null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsControllerCreatePlaylist>>,\n      TError,\n      { data: CreatePlaylistRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsControllerCreatePlaylist>>,\n  TError,\n  { data: CreatePlaylistRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsControllerCreatePlaylistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Update a playlist\n */\nexport const playlistsControllerUpdatePlaylist = (\n  playlistId: string,\n  updatePlaylistRequestPayload: UpdatePlaylistRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    {\n      url: `/playlists/${playlistId}`,\n      method: 'PUT',\n      headers: { 'Content-Type': 'application/json' },\n      data: updatePlaylistRequestPayload,\n    },\n    options\n  )\n}\n\nexport const getPlaylistsControllerUpdatePlaylistMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsControllerUpdatePlaylist>>,\n    TError,\n    { playlistId: string; data: UpdatePlaylistRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsControllerUpdatePlaylist>>,\n  TError,\n  { playlistId: string; data: UpdatePlaylistRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['playlistsControllerUpdatePlaylist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsControllerUpdatePlaylist>>,\n    { playlistId: string; data: UpdatePlaylistRequestPayload }\n  > = (props) => {\n    const { playlistId, data } = props ?? {}\n\n    return playlistsControllerUpdatePlaylist(playlistId, data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsControllerUpdatePlaylistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerUpdatePlaylist>>\n>\nexport type PlaylistsControllerUpdatePlaylistMutationBody = UpdatePlaylistRequestPayload\nexport type PlaylistsControllerUpdatePlaylistMutationError = null | null\n\n/**\n * @summary Update a playlist\n */\nexport const usePlaylistsControllerUpdatePlaylist = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsControllerUpdatePlaylist>>,\n      TError,\n      { playlistId: string; data: UpdatePlaylistRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsControllerUpdatePlaylist>>,\n  TError,\n  { playlistId: string; data: UpdatePlaylistRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsControllerUpdatePlaylistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Delete a playlist\n */\nexport const playlistsControllerDeletePlaylist = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/playlists/${playlistId}`, method: 'DELETE' }, options)\n}\n\nexport const getPlaylistsControllerDeletePlaylistMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsControllerDeletePlaylist>>,\n    TError,\n    { playlistId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsControllerDeletePlaylist>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationKey = ['playlistsControllerDeletePlaylist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsControllerDeletePlaylist>>,\n    { playlistId: string }\n  > = (props) => {\n    const { playlistId } = props ?? {}\n\n    return playlistsControllerDeletePlaylist(playlistId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsControllerDeletePlaylistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerDeletePlaylist>>\n>\n\nexport type PlaylistsControllerDeletePlaylistMutationError = null | null\n\n/**\n * @summary Delete a playlist\n */\nexport const usePlaylistsControllerDeletePlaylist = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsControllerDeletePlaylist>>,\n      TError,\n      { playlistId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsControllerDeletePlaylist>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsControllerDeletePlaylistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Reorder playlists\n */\nexport const playlistsControllerReorderPlaylist = (\n  playlistId: string,\n  reorderPlaylistsRequestPayload: ReorderPlaylistsRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    {\n      url: `/playlists/${playlistId}/reorder`,\n      method: 'PUT',\n      headers: { 'Content-Type': 'application/json' },\n      data: reorderPlaylistsRequestPayload,\n    },\n    options\n  )\n}\n\nexport const getPlaylistsControllerReorderPlaylistMutationOptions = <\n  TError = null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsControllerReorderPlaylist>>,\n    TError,\n    { playlistId: string; data: ReorderPlaylistsRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsControllerReorderPlaylist>>,\n  TError,\n  { playlistId: string; data: ReorderPlaylistsRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['playlistsControllerReorderPlaylist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsControllerReorderPlaylist>>,\n    { playlistId: string; data: ReorderPlaylistsRequestPayload }\n  > = (props) => {\n    const { playlistId, data } = props ?? {}\n\n    return playlistsControllerReorderPlaylist(playlistId, data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsControllerReorderPlaylistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerReorderPlaylist>>\n>\nexport type PlaylistsControllerReorderPlaylistMutationBody = ReorderPlaylistsRequestPayload\nexport type PlaylistsControllerReorderPlaylistMutationError = null\n\n/**\n * @summary Reorder playlists\n */\nexport const usePlaylistsControllerReorderPlaylist = <TError = null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsControllerReorderPlaylist>>,\n      TError,\n      { playlistId: string; data: ReorderPlaylistsRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsControllerReorderPlaylist>>,\n  TError,\n  { playlistId: string; data: ReorderPlaylistsRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsControllerReorderPlaylistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * Minimum height — 500px; image must be square\n * @summary Upload playlist cover\n */\nexport const playlistsControllerUploadMainImage = (\n  playlistId: string,\n  playlistsControllerUploadMainImageBody: PlaylistsControllerUploadMainImageBody,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  const formData = new FormData()\n  formData.append(`file`, playlistsControllerUploadMainImageBody.file)\n\n  return customInstance<GetImagesOutput>(\n    {\n      url: `/playlists/${playlistId}/images/main`,\n      method: 'POST',\n      headers: { 'Content-Type': 'multipart/form-data' },\n      data: formData,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getPlaylistsControllerUploadMainImageMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsControllerUploadMainImage>>,\n    TError,\n    { playlistId: string; data: PlaylistsControllerUploadMainImageBody },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsControllerUploadMainImage>>,\n  TError,\n  { playlistId: string; data: PlaylistsControllerUploadMainImageBody },\n  TContext\n> => {\n  const mutationKey = ['playlistsControllerUploadMainImage']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsControllerUploadMainImage>>,\n    { playlistId: string; data: PlaylistsControllerUploadMainImageBody }\n  > = (props) => {\n    const { playlistId, data } = props ?? {}\n\n    return playlistsControllerUploadMainImage(playlistId, data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsControllerUploadMainImageMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerUploadMainImage>>\n>\nexport type PlaylistsControllerUploadMainImageMutationBody = PlaylistsControllerUploadMainImageBody\nexport type PlaylistsControllerUploadMainImageMutationError = null | null\n\n/**\n * @summary Upload playlist cover\n */\nexport const usePlaylistsControllerUploadMainImage = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsControllerUploadMainImage>>,\n      TError,\n      { playlistId: string; data: PlaylistsControllerUploadMainImageBody },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsControllerUploadMainImage>>,\n  TError,\n  { playlistId: string; data: PlaylistsControllerUploadMainImageBody },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsControllerUploadMainImageMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Delete playlist cover\n */\nexport const playlistsControllerDeleteTrackCover = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    { url: `/playlists/${playlistId}/images/main`, method: 'DELETE' },\n    options\n  )\n}\n\nexport const getPlaylistsControllerDeleteTrackCoverMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsControllerDeleteTrackCover>>,\n    TError,\n    { playlistId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsControllerDeleteTrackCover>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationKey = ['playlistsControllerDeleteTrackCover']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsControllerDeleteTrackCover>>,\n    { playlistId: string }\n  > = (props) => {\n    const { playlistId } = props ?? {}\n\n    return playlistsControllerDeleteTrackCover(playlistId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsControllerDeleteTrackCoverMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsControllerDeleteTrackCover>>\n>\n\nexport type PlaylistsControllerDeleteTrackCoverMutationError = null | null\n\n/**\n * @summary Delete playlist cover\n */\nexport const usePlaylistsControllerDeleteTrackCover = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsControllerDeleteTrackCover>>,\n      TError,\n      { playlistId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsControllerDeleteTrackCover>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsControllerDeleteTrackCoverMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/playlists-public/playlists-public.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nimport type {\n  DataTag,\n  DefinedInitialDataOptions,\n  DefinedUseQueryResult,\n  MutationFunction,\n  QueryClient,\n  QueryFunction,\n  QueryKey,\n  UndefinedInitialDataOptions,\n  UseMutationOptions,\n  UseMutationResult,\n  UseQueryOptions,\n  UseQueryResult,\n} from '@tanstack/react-query'\nimport { useMutation, useQuery } from '@tanstack/react-query'\n\nimport { customInstance } from '.././custom-instance'\nimport type {\n  GetPlaylistOutput,\n  GetPlaylistsOutput,\n  PlaylistsPublicControllerGetPlaylistsParams,\n  ReactionOutput,\n} from '../musicfun.schemas'\n\ntype SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]\n\n/**\n * Query parameters must conform to the **GetPlaylistsRequestPayload** schema.\n * @summary Retrieve all playlists\n */\nexport const playlistsPublicControllerGetPlaylists = (\n  params?: PlaylistsPublicControllerGetPlaylistsParams,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetPlaylistsOutput>(\n    { url: `/playlists`, method: 'GET', params, signal },\n    options\n  )\n}\n\nexport const getPlaylistsPublicControllerGetPlaylistsQueryKey = (\n  params?: PlaylistsPublicControllerGetPlaylistsParams\n) => {\n  return [`/playlists`, ...(params ? [params] : [])] as const\n}\n\nexport const getPlaylistsPublicControllerGetPlaylistsQueryOptions = <\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n  TError = unknown,\n>(\n  params?: PlaylistsPublicControllerGetPlaylistsParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  }\n) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey =\n    queryOptions?.queryKey ?? getPlaylistsPublicControllerGetPlaylistsQueryKey(params)\n\n  const queryFn: QueryFunction<\n    Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>\n  > = ({ signal }) => playlistsPublicControllerGetPlaylists(params, requestOptions, signal)\n\n  return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type PlaylistsPublicControllerGetPlaylistsQueryResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>\n>\nexport type PlaylistsPublicControllerGetPlaylistsQueryError = unknown\n\nexport function usePlaylistsPublicControllerGetPlaylists<\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n  TError = unknown,\n>(\n  params: undefined | PlaylistsPublicControllerGetPlaylistsParams,\n  options: {\n    query: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n        TError,\n        TData\n      >\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n          TError,\n          Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function usePlaylistsPublicControllerGetPlaylists<\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n  TError = unknown,\n>(\n  params?: PlaylistsPublicControllerGetPlaylistsParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n        TError,\n        TData\n      >\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n          TError,\n          Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function usePlaylistsPublicControllerGetPlaylists<\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n  TError = unknown,\n>(\n  params?: PlaylistsPublicControllerGetPlaylistsParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary Retrieve all playlists\n */\n\nexport function usePlaylistsPublicControllerGetPlaylists<\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n  TError = unknown,\n>(\n  params?: PlaylistsPublicControllerGetPlaylistsParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylists>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getPlaylistsPublicControllerGetPlaylistsQueryOptions(params, options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Get a single playlist by ID\n */\nexport const playlistsPublicControllerGetPlaylistById = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetPlaylistOutput>(\n    { url: `/playlists/${playlistId}`, method: 'GET', signal },\n    options\n  )\n}\n\nexport const getPlaylistsPublicControllerGetPlaylistByIdQueryKey = (playlistId?: string) => {\n  return [`/playlists/${playlistId}`] as const\n}\n\nexport const getPlaylistsPublicControllerGetPlaylistByIdQueryOptions = <\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n  TError = null,\n>(\n  playlistId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  }\n) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey =\n    queryOptions?.queryKey ?? getPlaylistsPublicControllerGetPlaylistByIdQueryKey(playlistId)\n\n  const queryFn: QueryFunction<\n    Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>\n  > = ({ signal }) => playlistsPublicControllerGetPlaylistById(playlistId, requestOptions, signal)\n\n  return { queryKey, queryFn, enabled: !!playlistId, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type PlaylistsPublicControllerGetPlaylistByIdQueryResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>\n>\nexport type PlaylistsPublicControllerGetPlaylistByIdQueryError = null\n\nexport function usePlaylistsPublicControllerGetPlaylistById<\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n  TError = null,\n>(\n  playlistId: string,\n  options: {\n    query: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n        TError,\n        TData\n      >\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n          TError,\n          Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function usePlaylistsPublicControllerGetPlaylistById<\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n  TError = null,\n>(\n  playlistId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n        TError,\n        TData\n      >\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n          TError,\n          Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function usePlaylistsPublicControllerGetPlaylistById<\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n  TError = null,\n>(\n  playlistId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary Get a single playlist by ID\n */\n\nexport function usePlaylistsPublicControllerGetPlaylistById<\n  TData = Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n  TError = null,\n>(\n  playlistId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof playlistsPublicControllerGetPlaylistById>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getPlaylistsPublicControllerGetPlaylistByIdQueryOptions(playlistId, options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Like a playlist\n */\nexport const playlistsPublicControllerLikePlaylist = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/${playlistId}/likes`, method: 'POST', signal },\n    options\n  )\n}\n\nexport const getPlaylistsPublicControllerLikePlaylistMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsPublicControllerLikePlaylist>>,\n    TError,\n    { playlistId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsPublicControllerLikePlaylist>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationKey = ['playlistsPublicControllerLikePlaylist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsPublicControllerLikePlaylist>>,\n    { playlistId: string }\n  > = (props) => {\n    const { playlistId } = props ?? {}\n\n    return playlistsPublicControllerLikePlaylist(playlistId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsPublicControllerLikePlaylistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerLikePlaylist>>\n>\n\nexport type PlaylistsPublicControllerLikePlaylistMutationError = null | null | null\n\n/**\n * @summary Like a playlist\n */\nexport const usePlaylistsPublicControllerLikePlaylist = <\n  TError = null | null | null,\n  TContext = unknown,\n>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsPublicControllerLikePlaylist>>,\n      TError,\n      { playlistId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsPublicControllerLikePlaylist>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsPublicControllerLikePlaylistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Dislike a playlist\n */\nexport const playlistsPublicControllerDislikePlaylist = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/${playlistId}/dislikes`, method: 'POST', signal },\n    options\n  )\n}\n\nexport const getPlaylistsPublicControllerDislikePlaylistMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsPublicControllerDislikePlaylist>>,\n    TError,\n    { playlistId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsPublicControllerDislikePlaylist>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationKey = ['playlistsPublicControllerDislikePlaylist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsPublicControllerDislikePlaylist>>,\n    { playlistId: string }\n  > = (props) => {\n    const { playlistId } = props ?? {}\n\n    return playlistsPublicControllerDislikePlaylist(playlistId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsPublicControllerDislikePlaylistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerDislikePlaylist>>\n>\n\nexport type PlaylistsPublicControllerDislikePlaylistMutationError = null | null | null\n\n/**\n * @summary Dislike a playlist\n */\nexport const usePlaylistsPublicControllerDislikePlaylist = <\n  TError = null | null | null,\n  TContext = unknown,\n>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsPublicControllerDislikePlaylist>>,\n      TError,\n      { playlistId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsPublicControllerDislikePlaylist>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsPublicControllerDislikePlaylistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Remove user reaction from a playlist\n */\nexport const playlistsPublicControllerRemovePlaylistReaction = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/${playlistId}/reactions`, method: 'DELETE' },\n    options\n  )\n}\n\nexport const getPlaylistsPublicControllerRemovePlaylistReactionMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof playlistsPublicControllerRemovePlaylistReaction>>,\n    TError,\n    { playlistId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof playlistsPublicControllerRemovePlaylistReaction>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationKey = ['playlistsPublicControllerRemovePlaylistReaction']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof playlistsPublicControllerRemovePlaylistReaction>>,\n    { playlistId: string }\n  > = (props) => {\n    const { playlistId } = props ?? {}\n\n    return playlistsPublicControllerRemovePlaylistReaction(playlistId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type PlaylistsPublicControllerRemovePlaylistReactionMutationResult = NonNullable<\n  Awaited<ReturnType<typeof playlistsPublicControllerRemovePlaylistReaction>>\n>\n\nexport type PlaylistsPublicControllerRemovePlaylistReactionMutationError = null | null\n\n/**\n * @summary Remove user reaction from a playlist\n */\nexport const usePlaylistsPublicControllerRemovePlaylistReaction = <\n  TError = null | null,\n  TContext = unknown,\n>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof playlistsPublicControllerRemovePlaylistReaction>>,\n      TError,\n      { playlistId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof playlistsPublicControllerRemovePlaylistReaction>>,\n  TError,\n  { playlistId: string },\n  TContext\n> => {\n  const mutationOptions = getPlaylistsPublicControllerRemovePlaylistReactionMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/tags/tags.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nimport type {\n  DataTag,\n  DefinedInitialDataOptions,\n  DefinedUseQueryResult,\n  MutationFunction,\n  QueryClient,\n  QueryFunction,\n  QueryKey,\n  UndefinedInitialDataOptions,\n  UseMutationOptions,\n  UseMutationResult,\n  UseQueryOptions,\n  UseQueryResult,\n} from '@tanstack/react-query'\nimport { useMutation, useQuery } from '@tanstack/react-query'\n\nimport { customInstance } from '.././custom-instance'\nimport type {\n  CreateTagRequestPayload,\n  GetTagOutput,\n  TagsControllerSearchTagsParams,\n} from '../musicfun.schemas'\n\ntype SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]\n\n/**\n * @summary Create a new tag\n */\nexport const tagsControllerCreateTag = (\n  createTagRequestPayload: CreateTagRequestPayload,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetTagOutput>(\n    {\n      url: `/tags`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: createTagRequestPayload,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getTagsControllerCreateTagMutationOptions = <\n  TError = null | null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tagsControllerCreateTag>>,\n    TError,\n    { data: CreateTagRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tagsControllerCreateTag>>,\n  TError,\n  { data: CreateTagRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['tagsControllerCreateTag']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tagsControllerCreateTag>>,\n    { data: CreateTagRequestPayload }\n  > = (props) => {\n    const { data } = props ?? {}\n\n    return tagsControllerCreateTag(data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TagsControllerCreateTagMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tagsControllerCreateTag>>\n>\nexport type TagsControllerCreateTagMutationBody = CreateTagRequestPayload\nexport type TagsControllerCreateTagMutationError = null | null | null | null\n\n/**\n * @summary Create a new tag\n */\nexport const useTagsControllerCreateTag = <TError = null | null | null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tagsControllerCreateTag>>,\n      TError,\n      { data: CreateTagRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tagsControllerCreateTag>>,\n  TError,\n  { data: CreateTagRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getTagsControllerCreateTagMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Search tags by substring\n */\nexport const tagsControllerSearchTags = (\n  params: TagsControllerSearchTagsParams,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetTagOutput[]>(\n    { url: `/tags/search`, method: 'GET', params, signal },\n    options\n  )\n}\n\nexport const getTagsControllerSearchTagsQueryKey = (params?: TagsControllerSearchTagsParams) => {\n  return [`/tags/search`, ...(params ? [params] : [])] as const\n}\n\nexport const getTagsControllerSearchTagsQueryOptions = <\n  TData = Awaited<ReturnType<typeof tagsControllerSearchTags>>,\n  TError = null,\n>(\n  params: TagsControllerSearchTagsParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tagsControllerSearchTags>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  }\n) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey = queryOptions?.queryKey ?? getTagsControllerSearchTagsQueryKey(params)\n\n  const queryFn: QueryFunction<Awaited<ReturnType<typeof tagsControllerSearchTags>>> = ({\n    signal,\n  }) => tagsControllerSearchTags(params, requestOptions, signal)\n\n  return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof tagsControllerSearchTags>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type TagsControllerSearchTagsQueryResult = NonNullable<\n  Awaited<ReturnType<typeof tagsControllerSearchTags>>\n>\nexport type TagsControllerSearchTagsQueryError = null\n\nexport function useTagsControllerSearchTags<\n  TData = Awaited<ReturnType<typeof tagsControllerSearchTags>>,\n  TError = null,\n>(\n  params: TagsControllerSearchTagsParams,\n  options: {\n    query: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tagsControllerSearchTags>>, TError, TData>\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof tagsControllerSearchTags>>,\n          TError,\n          Awaited<ReturnType<typeof tagsControllerSearchTags>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useTagsControllerSearchTags<\n  TData = Awaited<ReturnType<typeof tagsControllerSearchTags>>,\n  TError = null,\n>(\n  params: TagsControllerSearchTagsParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tagsControllerSearchTags>>, TError, TData>\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof tagsControllerSearchTags>>,\n          TError,\n          Awaited<ReturnType<typeof tagsControllerSearchTags>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useTagsControllerSearchTags<\n  TData = Awaited<ReturnType<typeof tagsControllerSearchTags>>,\n  TError = null,\n>(\n  params: TagsControllerSearchTagsParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tagsControllerSearchTags>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary Search tags by substring\n */\n\nexport function useTagsControllerSearchTags<\n  TData = Awaited<ReturnType<typeof tagsControllerSearchTags>>,\n  TError = null,\n>(\n  params: TagsControllerSearchTagsParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tagsControllerSearchTags>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getTagsControllerSearchTagsQueryOptions(params, options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Delete a tag by ID\n */\nexport const tagsControllerDeleteTag = (\n  id: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/tags/${id}`, method: 'DELETE' }, options)\n}\n\nexport const getTagsControllerDeleteTagMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tagsControllerDeleteTag>>,\n    TError,\n    { id: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tagsControllerDeleteTag>>,\n  TError,\n  { id: string },\n  TContext\n> => {\n  const mutationKey = ['tagsControllerDeleteTag']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tagsControllerDeleteTag>>,\n    { id: string }\n  > = (props) => {\n    const { id } = props ?? {}\n\n    return tagsControllerDeleteTag(id, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TagsControllerDeleteTagMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tagsControllerDeleteTag>>\n>\n\nexport type TagsControllerDeleteTagMutationError = null | null | null\n\n/**\n * @summary Delete a tag by ID\n */\nexport const useTagsControllerDeleteTag = <TError = null | null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tagsControllerDeleteTag>>,\n      TError,\n      { id: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tagsControllerDeleteTag>>,\n  TError,\n  { id: string },\n  TContext\n> => {\n  const mutationOptions = getTagsControllerDeleteTagMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/tracks-owner/tracks-owner.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nimport type {\n  MutationFunction,\n  QueryClient,\n  UseMutationOptions,\n  UseMutationResult,\n} from '@tanstack/react-query'\nimport { useMutation } from '@tanstack/react-query'\n\nimport { customInstance } from '.././custom-instance'\nimport type {\n  AddTrackToPlaylistRequestPayload,\n  GetImagesOutput,\n  GetTrackOutput,\n  ReorderTracksRequestPayload,\n  TracksControllerUploadTrackCoverBody,\n  TracksControllerUploadTrackMp3Body,\n  UpdateTrackRequestPayload,\n} from '../musicfun.schemas'\n\ntype SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]\n\n/**\n * @summary Update track information\n */\nexport const tracksControllerUpdateTrack = (\n  trackId: string,\n  updateTrackRequestPayload: UpdateTrackRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<GetTrackOutput>(\n    {\n      url: `/playlists/tracks/${trackId}`,\n      method: 'PUT',\n      headers: { 'Content-Type': 'application/json' },\n      data: updateTrackRequestPayload,\n    },\n    options\n  )\n}\n\nexport const getTracksControllerUpdateTrackMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerUpdateTrack>>,\n    TError,\n    { trackId: string; data: UpdateTrackRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerUpdateTrack>>,\n  TError,\n  { trackId: string; data: UpdateTrackRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerUpdateTrack']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerUpdateTrack>>,\n    { trackId: string; data: UpdateTrackRequestPayload }\n  > = (props) => {\n    const { trackId, data } = props ?? {}\n\n    return tracksControllerUpdateTrack(trackId, data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerUpdateTrackMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerUpdateTrack>>\n>\nexport type TracksControllerUpdateTrackMutationBody = UpdateTrackRequestPayload\nexport type TracksControllerUpdateTrackMutationError = null | null | null\n\n/**\n * @summary Update track information\n */\nexport const useTracksControllerUpdateTrack = <TError = null | null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerUpdateTrack>>,\n      TError,\n      { trackId: string; data: UpdateTrackRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerUpdateTrack>>,\n  TError,\n  { trackId: string; data: UpdateTrackRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerUpdateTrackMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Permanently delete a track\n */\nexport const tracksControllerDeleteTrackCompletely = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>({ url: `/playlists/tracks/${trackId}`, method: 'DELETE' }, options)\n}\n\nexport const getTracksControllerDeleteTrackCompletelyMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerDeleteTrackCompletely>>,\n    TError,\n    { trackId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerDeleteTrackCompletely>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerDeleteTrackCompletely']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerDeleteTrackCompletely>>,\n    { trackId: string }\n  > = (props) => {\n    const { trackId } = props ?? {}\n\n    return tracksControllerDeleteTrackCompletely(trackId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerDeleteTrackCompletelyMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerDeleteTrackCompletely>>\n>\n\nexport type TracksControllerDeleteTrackCompletelyMutationError = null | null\n\n/**\n * @summary Permanently delete a track\n */\nexport const useTracksControllerDeleteTrackCompletely = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerDeleteTrackCompletely>>,\n      TError,\n      { trackId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerDeleteTrackCompletely>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerDeleteTrackCompletelyMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Reorder tracks in a playlist\n */\nexport const tracksControllerReorderTrack = (\n  playlistId: string,\n  trackId: string,\n  reorderTracksRequestPayload: ReorderTracksRequestPayload,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    {\n      url: `/playlists/${playlistId}/tracks/${trackId}/reorder`,\n      method: 'PUT',\n      headers: { 'Content-Type': 'application/json' },\n      data: reorderTracksRequestPayload,\n    },\n    options\n  )\n}\n\nexport const getTracksControllerReorderTrackMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerReorderTrack>>,\n    TError,\n    { playlistId: string; trackId: string; data: ReorderTracksRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerReorderTrack>>,\n  TError,\n  { playlistId: string; trackId: string; data: ReorderTracksRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerReorderTrack']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerReorderTrack>>,\n    { playlistId: string; trackId: string; data: ReorderTracksRequestPayload }\n  > = (props) => {\n    const { playlistId, trackId, data } = props ?? {}\n\n    return tracksControllerReorderTrack(playlistId, trackId, data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerReorderTrackMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerReorderTrack>>\n>\nexport type TracksControllerReorderTrackMutationBody = ReorderTracksRequestPayload\nexport type TracksControllerReorderTrackMutationError = null | null | null\n\n/**\n * @summary Reorder tracks in a playlist\n */\nexport const useTracksControllerReorderTrack = <TError = null | null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerReorderTrack>>,\n      TError,\n      { playlistId: string; trackId: string; data: ReorderTracksRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerReorderTrack>>,\n  TError,\n  { playlistId: string; trackId: string; data: ReorderTracksRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerReorderTrackMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Add a track to your playlist\n */\nexport const tracksControllerAddTrackToPlaylist = (\n  playlistId: string,\n  addTrackToPlaylistRequestPayload: AddTrackToPlaylistRequestPayload,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<null>(\n    {\n      url: `/playlists/${playlistId}/relationships/tracks`,\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      data: addTrackToPlaylistRequestPayload,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getTracksControllerAddTrackToPlaylistMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerAddTrackToPlaylist>>,\n    TError,\n    { playlistId: string; data: AddTrackToPlaylistRequestPayload },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerAddTrackToPlaylist>>,\n  TError,\n  { playlistId: string; data: AddTrackToPlaylistRequestPayload },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerAddTrackToPlaylist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerAddTrackToPlaylist>>,\n    { playlistId: string; data: AddTrackToPlaylistRequestPayload }\n  > = (props) => {\n    const { playlistId, data } = props ?? {}\n\n    return tracksControllerAddTrackToPlaylist(playlistId, data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerAddTrackToPlaylistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerAddTrackToPlaylist>>\n>\nexport type TracksControllerAddTrackToPlaylistMutationBody = AddTrackToPlaylistRequestPayload\nexport type TracksControllerAddTrackToPlaylistMutationError = null | null\n\n/**\n * @summary Add a track to your playlist\n */\nexport const useTracksControllerAddTrackToPlaylist = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerAddTrackToPlaylist>>,\n      TError,\n      { playlistId: string; data: AddTrackToPlaylistRequestPayload },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerAddTrackToPlaylist>>,\n  TError,\n  { playlistId: string; data: AddTrackToPlaylistRequestPayload },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerAddTrackToPlaylistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Remove a track from your playlist\n */\nexport const tracksControllerUnbindTrackFromPlaylist = (\n  playlistId: string,\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    { url: `/playlists/${playlistId}/relationships/tracks/${trackId}`, method: 'DELETE' },\n    options\n  )\n}\n\nexport const getTracksControllerUnbindTrackFromPlaylistMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerUnbindTrackFromPlaylist>>,\n    TError,\n    { playlistId: string; trackId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerUnbindTrackFromPlaylist>>,\n  TError,\n  { playlistId: string; trackId: string },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerUnbindTrackFromPlaylist']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerUnbindTrackFromPlaylist>>,\n    { playlistId: string; trackId: string }\n  > = (props) => {\n    const { playlistId, trackId } = props ?? {}\n\n    return tracksControllerUnbindTrackFromPlaylist(playlistId, trackId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerUnbindTrackFromPlaylistMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerUnbindTrackFromPlaylist>>\n>\n\nexport type TracksControllerUnbindTrackFromPlaylistMutationError = null | null\n\n/**\n * @summary Remove a track from your playlist\n */\nexport const useTracksControllerUnbindTrackFromPlaylist = <\n  TError = null | null,\n  TContext = unknown,\n>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerUnbindTrackFromPlaylist>>,\n      TError,\n      { playlistId: string; trackId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerUnbindTrackFromPlaylist>>,\n  TError,\n  { playlistId: string; trackId: string },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerUnbindTrackFromPlaylistMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Publish a track (make it publicly available)\n */\nexport const tracksControllerPublishTrack = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<null>(\n    { url: `/playlists/tracks/${trackId}/actions/publish`, method: 'POST', signal },\n    options\n  )\n}\n\nexport const getTracksControllerPublishTrackMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerPublishTrack>>,\n    TError,\n    { trackId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerPublishTrack>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerPublishTrack']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerPublishTrack>>,\n    { trackId: string }\n  > = (props) => {\n    const { trackId } = props ?? {}\n\n    return tracksControllerPublishTrack(trackId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerPublishTrackMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerPublishTrack>>\n>\n\nexport type TracksControllerPublishTrackMutationError = null | null | null\n\n/**\n * @summary Publish a track (make it publicly available)\n */\nexport const useTracksControllerPublishTrack = <TError = null | null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerPublishTrack>>,\n      TError,\n      { trackId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerPublishTrack>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerPublishTrackMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Upload track cover\n */\nexport const tracksControllerUploadTrackCover = (\n  trackId: string,\n  tracksControllerUploadTrackCoverBody: TracksControllerUploadTrackCoverBody,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  const formData = new FormData()\n  formData.append(`cover`, tracksControllerUploadTrackCoverBody.cover)\n\n  return customInstance<GetImagesOutput>(\n    {\n      url: `/playlists/tracks/${trackId}/cover`,\n      method: 'POST',\n      headers: { 'Content-Type': 'multipart/form-data' },\n      data: formData,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getTracksControllerUploadTrackCoverMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerUploadTrackCover>>,\n    TError,\n    { trackId: string; data: TracksControllerUploadTrackCoverBody },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerUploadTrackCover>>,\n  TError,\n  { trackId: string; data: TracksControllerUploadTrackCoverBody },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerUploadTrackCover']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerUploadTrackCover>>,\n    { trackId: string; data: TracksControllerUploadTrackCoverBody }\n  > = (props) => {\n    const { trackId, data } = props ?? {}\n\n    return tracksControllerUploadTrackCover(trackId, data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerUploadTrackCoverMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerUploadTrackCover>>\n>\nexport type TracksControllerUploadTrackCoverMutationBody = TracksControllerUploadTrackCoverBody\nexport type TracksControllerUploadTrackCoverMutationError = null | null | null\n\n/**\n * @summary Upload track cover\n */\nexport const useTracksControllerUploadTrackCover = <\n  TError = null | null | null,\n  TContext = unknown,\n>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerUploadTrackCover>>,\n      TError,\n      { trackId: string; data: TracksControllerUploadTrackCoverBody },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerUploadTrackCover>>,\n  TError,\n  { trackId: string; data: TracksControllerUploadTrackCoverBody },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerUploadTrackCoverMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Delete track cover\n */\nexport const tracksControllerDeleteTrackCover = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<null>(\n    { url: `/playlists/tracks/${trackId}/cover`, method: 'DELETE' },\n    options\n  )\n}\n\nexport const getTracksControllerDeleteTrackCoverMutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerDeleteTrackCover>>,\n    TError,\n    { trackId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerDeleteTrackCover>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerDeleteTrackCover']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerDeleteTrackCover>>,\n    { trackId: string }\n  > = (props) => {\n    const { trackId } = props ?? {}\n\n    return tracksControllerDeleteTrackCover(trackId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerDeleteTrackCoverMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerDeleteTrackCover>>\n>\n\nexport type TracksControllerDeleteTrackCoverMutationError = null | null\n\n/**\n * @summary Delete track cover\n */\nexport const useTracksControllerDeleteTrackCover = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerDeleteTrackCover>>,\n      TError,\n      { trackId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerDeleteTrackCover>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerDeleteTrackCoverMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Create a track with MP3 file upload\n */\nexport const tracksControllerUploadTrackMp3 = (\n  tracksControllerUploadTrackMp3Body: TracksControllerUploadTrackMp3Body,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  const formData = new FormData()\n  formData.append(`title`, tracksControllerUploadTrackMp3Body.title)\n  formData.append(`file`, tracksControllerUploadTrackMp3Body.file)\n\n  return customInstance<GetTrackOutput>(\n    {\n      url: `/playlists/tracks/upload`,\n      method: 'POST',\n      headers: { 'Content-Type': 'multipart/form-data' },\n      data: formData,\n      signal,\n    },\n    options\n  )\n}\n\nexport const getTracksControllerUploadTrackMp3MutationOptions = <\n  TError = null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksControllerUploadTrackMp3>>,\n    TError,\n    { data: TracksControllerUploadTrackMp3Body },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksControllerUploadTrackMp3>>,\n  TError,\n  { data: TracksControllerUploadTrackMp3Body },\n  TContext\n> => {\n  const mutationKey = ['tracksControllerUploadTrackMp3']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksControllerUploadTrackMp3>>,\n    { data: TracksControllerUploadTrackMp3Body }\n  > = (props) => {\n    const { data } = props ?? {}\n\n    return tracksControllerUploadTrackMp3(data, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksControllerUploadTrackMp3MutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksControllerUploadTrackMp3>>\n>\nexport type TracksControllerUploadTrackMp3MutationBody = TracksControllerUploadTrackMp3Body\nexport type TracksControllerUploadTrackMp3MutationError = null | null\n\n/**\n * @summary Create a track with MP3 file upload\n */\nexport const useTracksControllerUploadTrackMp3 = <TError = null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksControllerUploadTrackMp3>>,\n      TError,\n      { data: TracksControllerUploadTrackMp3Body },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksControllerUploadTrackMp3>>,\n  TError,\n  { data: TracksControllerUploadTrackMp3Body },\n  TContext\n> => {\n  const mutationOptions = getTracksControllerUploadTrackMp3MutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/orval/tracks-public/tracks-public.ts",
    "content": "/**\n * Generated by orval v7.11.2 🍺\n * Do not edit manually.\n * MusicFun API\n * API for learning. Create your own analogue of a popular music service, such as SoundCloud or Spotify.\n\n<h4>mp3 examples:</h4> \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3   \n🔈: https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3\n * OpenAPI spec version: 1.0\n */\nimport type {\n  DataTag,\n  DefinedInitialDataOptions,\n  DefinedUseQueryResult,\n  MutationFunction,\n  QueryClient,\n  QueryFunction,\n  QueryKey,\n  UndefinedInitialDataOptions,\n  UseMutationOptions,\n  UseMutationResult,\n  UseQueryOptions,\n  UseQueryResult,\n} from '@tanstack/react-query'\nimport { useMutation, useQuery } from '@tanstack/react-query'\n\nimport { customInstance } from '.././custom-instance'\nimport type {\n  GetPlaylistTrackListOutput,\n  GetTrackDetailsOutput,\n  GetTrackListOutput,\n  JsonApiErrorDocument,\n  ReactionOutput,\n  TracksPublicControllerGetAllTracksParams,\n} from '../musicfun.schemas'\n\ntype SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1]\n\n/**\n * @summary Get list of all tracks in all playlists\n */\nexport const tracksPublicControllerGetAllTracks = (\n  params?: TracksPublicControllerGetAllTracksParams,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetTrackListOutput>(\n    { url: `/playlists/tracks`, method: 'GET', params, signal },\n    options\n  )\n}\n\nexport const getTracksPublicControllerGetAllTracksQueryKey = (\n  params?: TracksPublicControllerGetAllTracksParams\n) => {\n  return [`/playlists/tracks`, ...(params ? [params] : [])] as const\n}\n\nexport const getTracksPublicControllerGetAllTracksQueryOptions = <\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>,\n  TError = JsonApiErrorDocument,\n>(\n  params?: TracksPublicControllerGetAllTracksParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  }\n) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey = queryOptions?.queryKey ?? getTracksPublicControllerGetAllTracksQueryKey(params)\n\n  const queryFn: QueryFunction<Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>> = ({\n    signal,\n  }) => tracksPublicControllerGetAllTracks(params, requestOptions, signal)\n\n  return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type TracksPublicControllerGetAllTracksQueryResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>\n>\nexport type TracksPublicControllerGetAllTracksQueryError = JsonApiErrorDocument\n\nexport function useTracksPublicControllerGetAllTracks<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>,\n  TError = JsonApiErrorDocument,\n>(\n  params: undefined | TracksPublicControllerGetAllTracksParams,\n  options: {\n    query: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>, TError, TData>\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>,\n          TError,\n          Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useTracksPublicControllerGetAllTracks<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>,\n  TError = JsonApiErrorDocument,\n>(\n  params?: TracksPublicControllerGetAllTracksParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>, TError, TData>\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>,\n          TError,\n          Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useTracksPublicControllerGetAllTracks<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>,\n  TError = JsonApiErrorDocument,\n>(\n  params?: TracksPublicControllerGetAllTracksParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary Get list of all tracks in all playlists\n */\n\nexport function useTracksPublicControllerGetAllTracks<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>,\n  TError = JsonApiErrorDocument,\n>(\n  params?: TracksPublicControllerGetAllTracksParams,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<Awaited<ReturnType<typeof tracksPublicControllerGetAllTracks>>, TError, TData>\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getTracksPublicControllerGetAllTracksQueryOptions(params, options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Get list of tracks in a playlist\n */\nexport const tracksPublicControllerGetPlaylistTracks = (\n  playlistId: string,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetPlaylistTrackListOutput>(\n    { url: `/playlists/${playlistId}/tracks`, method: 'GET', signal },\n    options\n  )\n}\n\nexport const getTracksPublicControllerGetPlaylistTracksQueryKey = (playlistId?: string) => {\n  return [`/playlists/${playlistId}/tracks`] as const\n}\n\nexport const getTracksPublicControllerGetPlaylistTracksQueryOptions = <\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n  TError = null,\n>(\n  playlistId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  }\n) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey =\n    queryOptions?.queryKey ?? getTracksPublicControllerGetPlaylistTracksQueryKey(playlistId)\n\n  const queryFn: QueryFunction<\n    Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>\n  > = ({ signal }) => tracksPublicControllerGetPlaylistTracks(playlistId, requestOptions, signal)\n\n  return { queryKey, queryFn, enabled: !!playlistId, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type TracksPublicControllerGetPlaylistTracksQueryResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>\n>\nexport type TracksPublicControllerGetPlaylistTracksQueryError = null\n\nexport function useTracksPublicControllerGetPlaylistTracks<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n  TError = null,\n>(\n  playlistId: string,\n  options: {\n    query: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n        TError,\n        TData\n      >\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n          TError,\n          Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useTracksPublicControllerGetPlaylistTracks<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n  TError = null,\n>(\n  playlistId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n        TError,\n        TData\n      >\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n          TError,\n          Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useTracksPublicControllerGetPlaylistTracks<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n  TError = null,\n>(\n  playlistId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary Get list of tracks in a playlist\n */\n\nexport function useTracksPublicControllerGetPlaylistTracks<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n  TError = null,\n>(\n  playlistId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetPlaylistTracks>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getTracksPublicControllerGetPlaylistTracksQueryOptions(playlistId, options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Get track details by ID\n */\nexport const tracksPublicControllerGetTrackDetails = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<GetTrackDetailsOutput>(\n    { url: `/playlists/tracks/${trackId}`, method: 'GET', signal },\n    options\n  )\n}\n\nexport const getTracksPublicControllerGetTrackDetailsQueryKey = (trackId?: string) => {\n  return [`/playlists/tracks/${trackId}`] as const\n}\n\nexport const getTracksPublicControllerGetTrackDetailsQueryOptions = <\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n  TError = null,\n>(\n  trackId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  }\n) => {\n  const { query: queryOptions, request: requestOptions } = options ?? {}\n\n  const queryKey =\n    queryOptions?.queryKey ?? getTracksPublicControllerGetTrackDetailsQueryKey(trackId)\n\n  const queryFn: QueryFunction<\n    Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>\n  > = ({ signal }) => tracksPublicControllerGetTrackDetails(trackId, requestOptions, signal)\n\n  return { queryKey, queryFn, enabled: !!trackId, ...queryOptions } as UseQueryOptions<\n    Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n    TError,\n    TData\n  > & { queryKey: DataTag<QueryKey, TData, TError> }\n}\n\nexport type TracksPublicControllerGetTrackDetailsQueryResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>\n>\nexport type TracksPublicControllerGetTrackDetailsQueryError = null\n\nexport function useTracksPublicControllerGetTrackDetails<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n  TError = null,\n>(\n  trackId: string,\n  options: {\n    query: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n        TError,\n        TData\n      >\n    > &\n      Pick<\n        DefinedInitialDataOptions<\n          Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n          TError,\n          Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useTracksPublicControllerGetTrackDetails<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n  TError = null,\n>(\n  trackId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n        TError,\n        TData\n      >\n    > &\n      Pick<\n        UndefinedInitialDataOptions<\n          Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n          TError,\n          Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>\n        >,\n        'initialData'\n      >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\nexport function useTracksPublicControllerGetTrackDetails<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n  TError = null,\n>(\n  trackId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }\n/**\n * @summary Get track details by ID\n */\n\nexport function useTracksPublicControllerGetTrackDetails<\n  TData = Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n  TError = null,\n>(\n  trackId: string,\n  options?: {\n    query?: Partial<\n      UseQueryOptions<\n        Awaited<ReturnType<typeof tracksPublicControllerGetTrackDetails>>,\n        TError,\n        TData\n      >\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {\n  const queryOptions = getTracksPublicControllerGetTrackDetailsQueryOptions(trackId, options)\n\n  const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {\n    queryKey: DataTag<QueryKey, TData, TError>\n  }\n\n  query.queryKey = queryOptions.queryKey\n\n  return query\n}\n\n/**\n * @summary Like or toggle like on a track\n */\nexport const tracksPublicControllerLikeTrack = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/tracks/${trackId}/likes`, method: 'POST', signal },\n    options\n  )\n}\n\nexport const getTracksPublicControllerLikeTrackMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksPublicControllerLikeTrack>>,\n    TError,\n    { trackId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksPublicControllerLikeTrack>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationKey = ['tracksPublicControllerLikeTrack']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksPublicControllerLikeTrack>>,\n    { trackId: string }\n  > = (props) => {\n    const { trackId } = props ?? {}\n\n    return tracksPublicControllerLikeTrack(trackId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksPublicControllerLikeTrackMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerLikeTrack>>\n>\n\nexport type TracksPublicControllerLikeTrackMutationError = null | null | null\n\n/**\n * @summary Like or toggle like on a track\n */\nexport const useTracksPublicControllerLikeTrack = <TError = null | null | null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksPublicControllerLikeTrack>>,\n      TError,\n      { trackId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksPublicControllerLikeTrack>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationOptions = getTracksPublicControllerLikeTrackMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Dislike or toggle dislike on a track\n */\nexport const tracksPublicControllerDislikeTrack = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>,\n  signal?: AbortSignal\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/tracks/${trackId}/dislikes`, method: 'POST', signal },\n    options\n  )\n}\n\nexport const getTracksPublicControllerDislikeTrackMutationOptions = <\n  TError = null | null | null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksPublicControllerDislikeTrack>>,\n    TError,\n    { trackId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksPublicControllerDislikeTrack>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationKey = ['tracksPublicControllerDislikeTrack']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksPublicControllerDislikeTrack>>,\n    { trackId: string }\n  > = (props) => {\n    const { trackId } = props ?? {}\n\n    return tracksPublicControllerDislikeTrack(trackId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksPublicControllerDislikeTrackMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerDislikeTrack>>\n>\n\nexport type TracksPublicControllerDislikeTrackMutationError = null | null | null\n\n/**\n * @summary Dislike or toggle dislike on a track\n */\nexport const useTracksPublicControllerDislikeTrack = <\n  TError = null | null | null,\n  TContext = unknown,\n>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksPublicControllerDislikeTrack>>,\n      TError,\n      { trackId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksPublicControllerDislikeTrack>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationOptions = getTracksPublicControllerDislikeTrackMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n/**\n * @summary Remove user reaction from a track\n */\nexport const tracksPublicControllerRemoveTrackReaction = (\n  trackId: string,\n  options?: SecondParameter<typeof customInstance>\n) => {\n  return customInstance<ReactionOutput>(\n    { url: `/playlists/tracks/${trackId}/reactions`, method: 'DELETE' },\n    options\n  )\n}\n\nexport const getTracksPublicControllerRemoveTrackReactionMutationOptions = <\n  TError = null,\n  TContext = unknown,\n>(options?: {\n  mutation?: UseMutationOptions<\n    Awaited<ReturnType<typeof tracksPublicControllerRemoveTrackReaction>>,\n    TError,\n    { trackId: string },\n    TContext\n  >\n  request?: SecondParameter<typeof customInstance>\n}): UseMutationOptions<\n  Awaited<ReturnType<typeof tracksPublicControllerRemoveTrackReaction>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationKey = ['tracksPublicControllerRemoveTrackReaction']\n  const { mutation: mutationOptions, request: requestOptions } = options\n    ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey\n      ? options\n      : { ...options, mutation: { ...options.mutation, mutationKey } }\n    : { mutation: { mutationKey }, request: undefined }\n\n  const mutationFn: MutationFunction<\n    Awaited<ReturnType<typeof tracksPublicControllerRemoveTrackReaction>>,\n    { trackId: string }\n  > = (props) => {\n    const { trackId } = props ?? {}\n\n    return tracksPublicControllerRemoveTrackReaction(trackId, requestOptions)\n  }\n\n  return { mutationFn, ...mutationOptions }\n}\n\nexport type TracksPublicControllerRemoveTrackReactionMutationResult = NonNullable<\n  Awaited<ReturnType<typeof tracksPublicControllerRemoveTrackReaction>>\n>\n\nexport type TracksPublicControllerRemoveTrackReactionMutationError = null\n\n/**\n * @summary Remove user reaction from a track\n */\nexport const useTracksPublicControllerRemoveTrackReaction = <TError = null, TContext = unknown>(\n  options?: {\n    mutation?: UseMutationOptions<\n      Awaited<ReturnType<typeof tracksPublicControllerRemoveTrackReaction>>,\n      TError,\n      { trackId: string },\n      TContext\n    >\n    request?: SecondParameter<typeof customInstance>\n  },\n  queryClient?: QueryClient\n): UseMutationResult<\n  Awaited<ReturnType<typeof tracksPublicControllerRemoveTrackReaction>>,\n  TError,\n  { trackId: string },\n  TContext\n> => {\n  const mutationOptions = getTracksPublicControllerRemoveTrackReactionMutationOptions(options)\n\n  return useMutation(mutationOptions, queryClient)\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/query-error-handler-for-rhf-factory.ts",
    "content": "import type { FieldValues, Path, UseFormSetError } from 'react-hook-form'\nimport { toast } from 'react-toastify'\n\nimport type { MutationMeta } from '../../app/routes/__root.tsx'\nimport {\n  isJsonApiErrorDocument,\n  type JsonApiErrorDocument,\n  parseJsonApiErrors,\n} from './json-api-error.ts'\n\nexport const queryErrorHandlerForRHFFactory = <T extends FieldValues>({\n  setError,\n}: {\n  setError?: UseFormSetError<T>\n}) => {\n  return (err: JsonApiErrorDocument) => {\n    // 400 от сервера в JSON:API формате\n    if (isJsonApiErrorDocument(err)) {\n      const { fieldErrors, globalErrors } = parseJsonApiErrors(err)\n\n      // полевые ошибки\n      for (const [field, message] of Object.entries(fieldErrors)) {\n        setError?.(field as Path<T>, { type: 'server', message })\n      }\n\n      // «глобальные» (без pointer)\n      if (globalErrors.length > 0) {\n        setError?.('root.server', {\n          type: 'server',\n          message: globalErrors.join('\\n'),\n        })\n        toast(globalErrors.join('\\n'))\n      }\n\n      return\n    }\n  }\n}\n\nexport const mutationGlobalErrorHandler = (\n  error: Error,\n  _: unknown,\n  __: unknown,\n  mutation: unknown\n) => {\n  // 400 от сервера в JSON:API формате\n  // @ts-expect-error ignore typing\n  const globalFlag = (mutation.meta as MutationMeta)?.globalErrorHandler\n  // если в meta сказали \"off\" — ничего не делаем\n  if (globalFlag === 'off') {\n    return\n  }\n\n  if (isJsonApiErrorDocument(error)) {\n    const { globalErrors } = parseJsonApiErrors(error)\n\n    // «глобальные» (без pointer)\n    if (globalErrors.length > 0) {\n      toast(globalErrors.join('\\n'))\n    }\n  }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/request-wrapper.ts",
    "content": "// types/api.ts\n\nimport { type ExtractError } from './json-api-error.ts'\n\n//-----------------------------------------------------------------------------\n// utils/requestWrapper.ts\n//-----------------------------------------------------------------------------\n// «Умный» обёртчик: Infers Data и Error из P,\n// возвращает Promise<Data>, а в случае ошибки — throw Error\nexport type ExtractData<T> = T extends { data?: infer D } ? NonNullable<D> : never\n\nexport async function requestWrapper<P extends Promise<{ data?: unknown; error?: unknown }>>(\n  promise: P\n): Promise<ExtractData<Awaited<P>>> {\n  const res = (await promise) as Awaited<P>\n  if ((res as { error?: unknown }).error) {\n    // здесь E = ExtractError<Awaited<P>>\n    throw (res as { error: ExtractError<Awaited<P>> }).error\n  }\n  return (res as { data: ExtractData<Awaited<P>> }).data\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/schema.ts",
    "content": "/**\n * This file was auto-generated by openapi-typescript.\n * Do not make direct changes to the file.\n */\n\nexport interface paths {\n  '/playlists/my': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Получить список моих плейлистов\n     * @deprecated\n     */\n    get: operations['PlaylistsController_getMyPlaylists']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить список всех плейлистов */\n    get: operations['PlaylistsPublicController_getPlaylists']\n    put?: never\n    /** Создать новый плейлист */\n    post: operations['PlaylistsController_createPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить один плейлист по ID */\n    get: operations['PlaylistsPublicController_getPlaylistById']\n    /** Обновить плейлист */\n    put: operations['PlaylistsController_updatePlaylist']\n    post?: never\n    /** Удалить плейлист */\n    delete: operations['PlaylistsController_deletePlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Переупорядочить плейлисты */\n    put: operations['PlaylistsController_reorderPlaylist']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/images/main': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /**\n     * Загрузить обложку для плейлиста\n     * @description Минимальная высота — 500px, квадратное изображение\n     */\n    post: operations['PlaylistsController_uploadMainImage']\n    /** Удалить обложку плейлиста */\n    delete: operations['PlaylistsController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить список всех треков во всех плейлистах */\n    get: operations['TracksPublicController_getAllTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить список треков внутри плейлиста */\n    get: operations['TracksPublicController_getPlaylistTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить детали трека по ID */\n    get: operations['TracksPublicController_getTrackDetails']\n    /** Обновить информацию о треке */\n    put: operations['TracksController_updateTrack']\n    post?: never\n    /** Полностью удалить трек */\n    delete: operations['TracksController_deleteTrackCompletely']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Поставить лайк треку или снять его (toggle) */\n    post: operations['TracksPublicController_likeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Поставить дизлайк треку или снять его (toggle) */\n    post: operations['TracksPublicController_dislikeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить реакцию пользователя на трек */\n    delete: operations['TracksPublicController_removeTrackReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Поставить лайк плейлисту */\n    post: operations['PlaylistsPublicController_likePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Поставить дизлайк плейлисту */\n    post: operations['PlaylistsPublicController_dislikePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить реакцию пользователя на плейлист */\n    delete: operations['PlaylistsPublicController_removePlaylistReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks/{trackId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Изменить порядок треков в плейлисте */\n    put: operations['TracksController_reorderTrack']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Добавить трек в свой плейлист */\n    post: operations['TracksController_addTrackToPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить трек из своего плейлиста */\n    delete: operations['TracksController_unbindTrackFromPlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/actions/publish': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Публикация трека (сделать доступным для всех) */\n    post: operations['TracksController_publishTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/cover': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Загрузить обложку трека */\n    post: operations['TracksController_uploadTrackCover']\n    /** Удалить обложку трека */\n    delete: operations['TracksController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/upload': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Создать трек с загрузкой mp3 файла */\n    post: operations['TracksController_uploadTrackMp3']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Создать нового исполнителя */\n    post: operations['ArtistsController_createArtist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Поиск исполнителей по подстроке */\n    get: operations['ArtistsController_searchArtist']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить исполнителя по ID */\n    delete: operations['ArtistsController_deleteArtist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/oauth-redirect': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * OAuth редирект\n     * @description The callback URL to redirect after grand access,\n     *          <a target=\"_blank\" href=\"https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\">https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid</a>\n     */\n    get: operations['AuthController_OauthRedirect']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/login': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Залогиниться с помощью кода, полученного после редиректа после авторизации через OAuth */\n    post: operations['AuthController_login']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/refresh': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Обновить пару refresh/access токенов */\n    post: operations['AuthController_refresh']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/logout': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Деактивировать refresh-token */\n    post: operations['AuthController_logout']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/me': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить текущего пользователя по access токену */\n    get: operations['AuthController_getMe']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Создать новый тег */\n    post: operations['TagsController_createTag']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Поиск тегов по подстроке */\n    get: operations['TagsController_searchTags']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить тег по ID */\n    delete: operations['TagsController_deleteTag']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n}\nexport type webhooks = Record<string, never>\nexport interface components {\n  schemas: {\n    UserOutputDTO: {\n      id: string\n      name: string\n    }\n    ImageDto: {\n      /** @enum {string} */\n      type: 'original' | 'thumbnail' | 'medium'\n      width: number\n      height: number\n      fileSize: number\n      url: string\n    }\n    PlaylistImagesOutputDTO: {\n      /** @description Оригинальное изображение и превьюшки */\n      main?: components['schemas']['ImageDto'][]\n    }\n    /**\n     * @description 0 (не залогинен или не реагировал), 1 — лайк, -1 — дизлайк\n     * @enum {number}\n     */\n    ReactionValue: 0 | 1 | -1\n    PlaylistAttributesDto: {\n      title: string\n      description: string | null\n      addedAt: string\n      updatedAt: string\n      order: number\n      user: components['schemas']['UserOutputDTO']\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      tags: string[]\n      likesCount: number\n      dislikesCount: number\n      /** @description 0 (не залогинен или не реагировал), 1 — лайк, -1 — дизлайк */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistListItemJsonApiData: {\n      id: string\n      /** @example playlists */\n      type: string\n      attributes: components['schemas']['PlaylistAttributesDto']\n    }\n    GetMyPlaylistsOutput: {\n      data: components['schemas']['PlaylistListItemJsonApiData'][]\n    }\n    CreatePlaylistRequestPayload: {\n      title: string\n      description?: string\n    }\n    PlaylistOutputAttributes: {\n      title: string\n      description: string | null\n      addedAt: string\n      updatedAt: string\n      order: number\n      user: components['schemas']['UserOutputDTO']\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      tags: string[]\n      likesCount: number\n      dislikesCount: number\n      /** @description 0 (не залогинен или не реагировал), 1 — лайк, -1 — дизлайк */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistOutput: {\n      id: string\n      /** @example playlists */\n      type: string\n      attributes: components['schemas']['PlaylistOutputAttributes']\n    }\n    GetPlaylistOutput: {\n      data: components['schemas']['PlaylistOutput']\n    }\n    UpdatePlaylistRequestPayload: {\n      title: string\n      /** @example Cool playlist */\n      description?: string | null\n      tagIds?: string[]\n    }\n    ReorderPlaylistsRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID плейлиста, после которого нужно вставить текущий. null - разместить плейлист в начало списка.\n       * @example a1b2c3d4-e5f6-7890-abcd-1234567890ef\n       */\n      putAfterItemId?: string | null\n    }\n    GetImagesOutput: {\n      /** @description Должен содержать оригинальный размер изображения и миниатюры, например: original, 320x180 и т.п. */\n      main?: components['schemas']['ImageDto'][]\n    }\n    GetTracksRequestPayload: {\n      /**\n       * @description Номер страницы для пагинации (начиная с 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Размер страницы для пагинации (от 1 до 20)\n       * @default 10\n       */\n      pageSize: number\n      /** @description Строка для поиска по названию плейлиста */\n      search?: string\n      /**\n       * @description Поле, по которому сортируются треки\n       * @default publishedAt\n       * @enum {string}\n       */\n      sortBy: 'publishedAt' | 'likesCount'\n      /**\n       * @description Направление сортировки (по возрастанию или убыванию)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: 'asc' | 'desc'\n      /** @description Фильтрация по ID тегов (можно передавать несколько) */\n      tagsIds?: string[]\n      /** @description Фильтрация по ID артистов (можно передавать несколько) */\n      artistsIds?: string[]\n      /** @description Фильтрация по ID пользователя (создателя трека) */\n      userId?: string\n      /** @description Если true — включать в выдачу также ваши неопубликованные треки (drafts) */\n      includeOwnUnpublished?: boolean\n      /**\n       * @description Тип пагинации: `offset` — по номеру страницы; `cursor` — keyset/seek.\n       * @default offset\n       * @enum {string}\n       */\n      paginationType: 'offset' | 'cursor'\n      /** @description Base64-закодированный курсор для keyset-пагинации. Используется только если paginationType=cursor. */\n      cursor?: string\n    }\n    AttachmentDto: {\n      id: string\n      /** Format: date-time */\n      addedAt: string\n      /** Format: date-time */\n      updatedAt: string\n      version: number\n      /**\n       * @description Public URL to access the uploaded file\n       * @example https://cdn.example.com/uploads/track123/cover.jpg\n       */\n      url: string\n      /**\n       * @description MIME type of the file\n       * @example image/jpeg\n       */\n      contentType: string\n      /**\n       * @description Original filename uploaded by the user\n       * @example cover.jpg\n       */\n      originalName: string\n      /**\n       * @description Size of the file in bytes\n       * @example 34872\n       */\n      fileSize: number\n    }\n    TrackListItemOutputAttributes: {\n      title: string\n      addedAt: string\n      attachments: components['schemas']['AttachmentDto'][]\n      images: components['schemas']['GetImagesOutput']\n      user: components['schemas']['UserOutputDTO']\n      /**\n       * @description 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n      isPublished: boolean\n      publishedAt?: string\n    }\n    ArtistRelationship: {\n      id: string\n      type: string\n    }\n    ArtistsRelationship: {\n      data: components['schemas']['ArtistRelationship'][]\n    }\n    TrackRelationships: {\n      artists: components['schemas']['ArtistsRelationship']\n    }\n    TrackListItemOutput: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackListItemOutputAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMetaWithPagingAndCursor: {\n      page: number\n      pageSize: number\n      totalCount: number | null\n      pagesCount: number | null\n      nextCursor: string | null\n    }\n    OmitTypeClass: {\n      name: string\n    }\n    IncludedArtistOutput: {\n      id: string\n      type: string\n      attributes: components['schemas']['OmitTypeClass']\n    }\n    GetTrackListOutput: {\n      data: components['schemas']['TrackListItemOutput'][]\n      meta: components['schemas']['JsonApiMetaWithPagingAndCursor']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    PlaylistTrackAttributes: {\n      title: string\n      order: number\n      addedAt: string\n      updatedAt: string\n      attachments: unknown[][]\n      images: components['schemas']['GetImagesOutput']\n      /**\n       * @description 0 (не залогинен или не реагировал), 1 — лайк, -1 — дизлайк\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n    }\n    GetPlaylistTrackListOutputData: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['PlaylistTrackAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMeta: {\n      totalCount: number\n    }\n    GetPlaylistTrackListOutput: {\n      data: components['schemas']['GetPlaylistTrackListOutputData'][]\n      meta: components['schemas']['JsonApiMeta']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    GetTagOutput: {\n      id: string\n      name: string\n    }\n    GetArtistOutput: {\n      id: string\n      name: string\n    }\n    TrackDetailsAttributes: {\n      title: string\n      lyrics?: string\n      releaseDate?: string\n      addedAt: string\n      /** Format: iso8601 */\n      updatedAt: string\n      duration: number\n      likesCount: number\n      dislikesCount: number\n      attachments: components['schemas']['AttachmentDto'][]\n      images: components['schemas']['GetImagesOutput']\n      tags: components['schemas']['GetTagOutput'][]\n      artists: components['schemas']['GetArtistOutput'][]\n      isPublished: boolean\n      publishedAt?: string\n      /**\n       * @description 0 – гость или не реагировал, 1 – пользователь лайкнул, -1 – пользователь дизлайкнул\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n    }\n    TrackDetailsData: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackDetailsAttributes']\n    }\n    GetTrackDetailsOutput: {\n      data: components['schemas']['TrackDetailsData']\n    }\n    ReactionOutput: {\n      objectId: string\n      /** @enum {number} */\n      value: 0 | 1 | -1\n      likes: number\n      dislikes: number\n    }\n    GetPlaylistsRequestPayload: {\n      /**\n       * @description Номер страницы для пагинации (начиная с 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Размер страницы для пагинации (от 1 до 20)\n       * @default 10\n       */\n      pageSize: number\n      /** @description Строка для поиска по названию плейлиста */\n      search?: string\n      /**\n       * @description Поле, по которому выполняется сортировка\n       * @default publishedAt\n       * @enum {string}\n       */\n      sortBy: 'publishedAt' | 'likesCount'\n      /**\n       * @description Направление сортировки (по возрастанию или убыванию)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: 'asc' | 'desc'\n      /** @description Фильтрация по ID тегов. Может быть передано несколько значений: tagsIds=tag1&tagsIds=tag2 */\n      tagsIds?: string[]\n      /** @description Фильтрация по ID пользователя (создателя плейлиста) */\n      userId?: string\n      /** @description Фильтрация по ID трека — только те плейлисты, в которых он содержится */\n      trackId?: string\n    }\n    JsonApiMetaWithPaging: {\n      totalCount: number\n      page: number\n      pageSize: number\n      pagesCount: number\n    }\n    GetPlaylistsOutput: {\n      data: components['schemas']['PlaylistListItemJsonApiData'][]\n      meta: components['schemas']['JsonApiMetaWithPaging']\n    }\n    ReorderTracksRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID трека, после которого нужно вставить текущий. null - разместить трек в начало списка.\n       * @example a1b2c3d4-e5f6-7890-abcd-1234567890ef\n       */\n      putAfterItemId?: string | null\n    }\n    UpdateTrackRequestPayload: {\n      title: string\n      /** @description Текст песни (lyrics) */\n      lyrics?: string\n      /** Format: iso8601 */\n      releaseDate?: string\n      tagIds?: string[]\n      artistsIds?: string[]\n    }\n    TrackOutputAttributes: {\n      title: string\n      lyrics?: string\n      releaseDate?: string\n      addedAt: string\n      /** Format: iso8601 */\n      updatedAt: string\n      duration: number\n      likesCount: number\n      dislikesCount: number\n      attachments: components['schemas']['AttachmentDto'][]\n      images: components['schemas']['GetImagesOutput']\n      tags: components['schemas']['GetTagOutput'][]\n      artists: components['schemas']['GetArtistOutput'][]\n      isPublished: boolean\n      publishedAt?: string\n      /**\n       * @description 0 – гость или не реагировал, 1 – пользователь лайкнул, -1 – пользователь дизлайкнул\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n    }\n    TrackOutput: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackOutputAttributes']\n    }\n    GetTrackOutput: {\n      data: components['schemas']['TrackOutput']\n    }\n    AddTrackToPlaylistRequestPayload: {\n      /** @description ID of the track to add to the playlist */\n      trackId: string\n    }\n    CreateArtistRequestPayload: {\n      name: string\n    }\n    LoginRequestPayload: {\n      /** @description Код, полученный от oauth-сервер после редиректа */\n      code: string\n      /**\n       * @description Укажите тоже значение, что и во время первого запроса на oauth-сервер\n       * @example http://localhost:3000/oauth2/callback\n       */\n      redirectUri: string\n      /**\n       * @description Срок жизни accessToken-а (по дефолту \"3m\"), Можно использовать значение в формате: be a string like \"60s\", \"3m\", \"2h\", \"1d\"\n       * @example 3m\n       */\n      accessTokenTTL?: string\n      /** @description Как долго будет жить refreshToken. Если true - 1 месяц, если false - 30 минут. Явно указанный accessTokenTTL не должен быть больше, чем время жизни refreshToken */\n      rememberMe: boolean\n    }\n    RefreshOutput: {\n      refreshToken: string\n      accessToken: string\n    }\n    BadRequestException: Record<string, never>\n    UnauthorizedException: Record<string, never>\n    RefreshRequestPayload: {\n      refreshToken: string\n    }\n    LogoutRequestPayload: {\n      refreshToken: string\n    }\n    GetMeOutput: {\n      userId: string\n      login: string\n    }\n    CreateTagRequestPayload: {\n      name: string\n    }\n    /**\n     * Format: binary\n     * @description Файл в multipart/form-data\n     */\n    BinaryFile: string\n  }\n  responses: never\n  parameters: never\n  requestBodies: never\n  headers: never\n  pathItems: never\n}\nexport type SchemaUserOutputDto = components['schemas']['UserOutputDTO']\nexport type SchemaImageDto = components['schemas']['ImageDto']\nexport type SchemaPlaylistImagesOutputDto = components['schemas']['PlaylistImagesOutputDTO']\nexport type SchemaReactionValue = components['schemas']['ReactionValue']\nexport type SchemaPlaylistAttributesDto = components['schemas']['PlaylistAttributesDto']\nexport type SchemaPlaylistListItemJsonApiData = components['schemas']['PlaylistListItemJsonApiData']\nexport type SchemaGetMyPlaylistsOutput = components['schemas']['GetMyPlaylistsOutput']\nexport type SchemaCreatePlaylistRequestPayload =\n  components['schemas']['CreatePlaylistRequestPayload']\nexport type SchemaPlaylistOutputAttributes = components['schemas']['PlaylistOutputAttributes']\nexport type SchemaPlaylistOutput = components['schemas']['PlaylistOutput']\nexport type SchemaGetPlaylistOutput = components['schemas']['GetPlaylistOutput']\nexport type SchemaUpdatePlaylistRequestPayload =\n  components['schemas']['UpdatePlaylistRequestPayload']\nexport type SchemaReorderPlaylistsRequestPayload =\n  components['schemas']['ReorderPlaylistsRequestPayload']\nexport type SchemaGetImagesOutput = components['schemas']['GetImagesOutput']\nexport type SchemaGetTracksRequestPayload = components['schemas']['GetTracksRequestPayload']\nexport type SchemaAttachmentDto = components['schemas']['AttachmentDto']\nexport type SchemaTrackListItemOutputAttributes =\n  components['schemas']['TrackListItemOutputAttributes']\nexport type SchemaArtistRelationship = components['schemas']['ArtistRelationship']\nexport type SchemaArtistsRelationship = components['schemas']['ArtistsRelationship']\nexport type SchemaTrackRelationships = components['schemas']['TrackRelationships']\nexport type SchemaTrackListItemOutput = components['schemas']['TrackListItemOutput']\nexport type SchemaJsonApiMetaWithPagingAndCursor =\n  components['schemas']['JsonApiMetaWithPagingAndCursor']\nexport type SchemaOmitTypeClass = components['schemas']['OmitTypeClass']\nexport type SchemaIncludedArtistOutput = components['schemas']['IncludedArtistOutput']\nexport type SchemaGetTrackListOutput = components['schemas']['GetTrackListOutput']\nexport type SchemaPlaylistTrackAttributes = components['schemas']['PlaylistTrackAttributes']\nexport type SchemaGetPlaylistTrackListOutputData =\n  components['schemas']['GetPlaylistTrackListOutputData']\nexport type SchemaJsonApiMeta = components['schemas']['JsonApiMeta']\nexport type SchemaGetPlaylistTrackListOutput = components['schemas']['GetPlaylistTrackListOutput']\nexport type SchemaGetTagOutput = components['schemas']['GetTagOutput']\nexport type SchemaGetArtistOutput = components['schemas']['GetArtistOutput']\nexport type SchemaTrackDetailsAttributes = components['schemas']['TrackDetailsAttributes']\nexport type SchemaTrackDetailsData = components['schemas']['TrackDetailsData']\nexport type SchemaGetTrackDetailsOutput = components['schemas']['GetTrackDetailsOutput']\nexport type SchemaReactionOutput = components['schemas']['ReactionOutput']\nexport type SchemaGetPlaylistsRequestPayload = components['schemas']['GetPlaylistsRequestPayload']\nexport type SchemaJsonApiMetaWithPaging = components['schemas']['JsonApiMetaWithPaging']\nexport type SchemaGetPlaylistsOutput = components['schemas']['GetPlaylistsOutput']\nexport type SchemaReorderTracksRequestPayload = components['schemas']['ReorderTracksRequestPayload']\nexport type SchemaUpdateTrackRequestPayload = components['schemas']['UpdateTrackRequestPayload']\nexport type SchemaTrackOutputAttributes = components['schemas']['TrackOutputAttributes']\nexport type SchemaTrackOutput = components['schemas']['TrackOutput']\nexport type SchemaGetTrackOutput = components['schemas']['GetTrackOutput']\nexport type SchemaAddTrackToPlaylistRequestPayload =\n  components['schemas']['AddTrackToPlaylistRequestPayload']\nexport type SchemaCreateArtistRequestPayload = components['schemas']['CreateArtistRequestPayload']\nexport type SchemaLoginRequestPayload = components['schemas']['LoginRequestPayload']\nexport type SchemaRefreshOutput = components['schemas']['RefreshOutput']\nexport type SchemaBadRequestException = components['schemas']['BadRequestException']\nexport type SchemaUnauthorizedException = components['schemas']['UnauthorizedException']\nexport type SchemaRefreshRequestPayload = components['schemas']['RefreshRequestPayload']\nexport type SchemaLogoutRequestPayload = components['schemas']['LogoutRequestPayload']\nexport type SchemaGetMeOutput = components['schemas']['GetMeOutput']\nexport type SchemaCreateTagRequestPayload = components['schemas']['CreateTagRequestPayload']\nexport type SchemaBinaryFile = components['schemas']['BinaryFile']\nexport type $defs = Record<string, never>\nexport interface operations {\n  PlaylistsController_getMyPlaylists: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Список плейлистов успешно получен */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMyPlaylistsOutput']\n        }\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylists: {\n    parameters: {\n      query?: {\n        /** @description Номер страницы для пагинации (начиная с 1) */\n        pageNumber?: number\n        /** @description Размер страницы для пагинации (от 1 до 20) */\n        pageSize?: number\n        /** @description Строка для поиска по названию плейлиста */\n        search?: string\n        /** @description Поле, по которому выполняется сортировка */\n        sortBy?: 'publishedAt' | 'likesCount'\n        /** @description Направление сортировки (по возрастанию или убыванию) */\n        sortDirection?: 'asc' | 'desc'\n        /** @description Фильтрация по ID тегов. Может быть передано несколько значений: tagsIds=tag1&tagsIds=tag2 */\n        tagsIds?: string[]\n        /** @description Фильтрация по ID пользователя (создателя плейлиста) */\n        userId?: string\n        /** @description Фильтрация по ID трека — только те плейлисты, в которых он содержится */\n        trackId?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: JSON:API список плейлистов с пагинацией */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistsOutput']\n        }\n      }\n    }\n  }\n  PlaylistsController_createPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Плейлист успешно создан */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Forbidden: Превышен лимит создания плейлистов */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylistById: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID плейлиста */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Плейлист успешно найден */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description NotFound: Плейлист с таким ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_updatePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description NoContent: Плейлист успешно обновлён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description BadRequest: Ошибка валидации, например, превышено количество тегов (более 5) */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Пользователь не имеет прав для обновления данного плейлиста */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deletePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Плейлист успешно удалён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Недостаточно прав для удаления плейлиста */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_reorderPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderPlaylistsRequestPayload']\n      }\n    }\n    responses: {\n      /** @description NoContent: Порядок плейлистов успешно изменён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Playlist not found или putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_uploadMainImage: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @description Максимальный размер 1 MB, Минимальная высота — 500px, квадратное изображение */\n          file: components['schemas']['BinaryFile']\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Обложка успешно загружена */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetImagesOutput']\n        }\n      }\n      /** @description BadRequest: Ошибка формата или размеров изображения */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нет прав на загрузку изображения в плейлист */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Обложка удалена */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Удаление обложки плейлиста другого пользователя запрещена */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getAllTracks: {\n    parameters: {\n      query?: {\n        /** @description Номер страницы для пагинации (начиная с 1) */\n        pageNumber?: number\n        /** @description Размер страницы для пагинации (от 1 до 20) */\n        pageSize?: number\n        /** @description Строка для поиска по названию плейлиста */\n        search?: string\n        /** @description Поле, по которому сортируются треки */\n        sortBy?: 'publishedAt' | 'likesCount'\n        /** @description Направление сортировки (по возрастанию или убыванию) */\n        sortDirection?: 'asc' | 'desc'\n        /** @description Фильтрация по ID тегов (можно передавать несколько) */\n        tagsIds?: string[]\n        /** @description Фильтрация по ID артистов (можно передавать несколько) */\n        artistsIds?: string[]\n        /** @description Фильтрация по ID пользователя (создателя трека) */\n        userId?: string\n        /** @description Если true — включать в выдачу также ваши неопубликованные треки (drafts) */\n        includeOwnUnpublished?: boolean\n        /** @description Тип пагинации: `offset` — по номеру страницы; `cursor` — keyset/seek. */\n        paginationType?: 'offset' | 'cursor'\n        /** @description Base64-закодированный курсор для keyset-пагинации. Используется только если paginationType=cursor. */\n        cursor?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Пагинированный список треков */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackListOutput']\n        }\n      }\n    }\n  }\n  TracksPublicController_getPlaylistTracks: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID плейлиста, для которого необходимо получить треки */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Список треков в плейлисте */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistTrackListOutput']\n        }\n      }\n      /** @description NotFound: Плейлист с указанным ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getTrackDetails: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID трека, для которого необходимо получить детали */\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Детали трека с вложениями */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description NotFound: Трек с таким ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_updateTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdateTrackRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Трек успешно обновлён */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackOutput']\n        }\n      }\n      /** @description BadRequest: Превышено количество тегов или артистов */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нельзя редактировать чужой трек */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек или плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCompletely: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Трек полностью удалён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Удаление чужого трека запрещено */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_likeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Текущая реакция пользователя + суммарные счётчики */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description BadRequest: Некорректный идентификатор */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_dislikeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description BadRequest: Некорректный ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_removeTrackReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_likePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description BadRequest: Некорректный ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_dislikePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description BadRequest: Некорректный ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_removePlaylistReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_reorderTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderTracksRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Порядок трека обновлён */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description BadRequest: Нельзя поставить после самого себя */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нет доступа к плейлисту */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек или putAfterItemId не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_addTrackToPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['AddTrackToPlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description NoContent: Трек успешно добавлен в плейлист */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нет доступа к плейлисту или превышен лимит треков в плейлисте: максимум 10 треков */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_unbindTrackFromPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Трек удалён из плейлиста */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нет доступа к плейлисту */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_publishTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Трек успешно опубликован */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нельзя публиковать чужие треки */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Трек с указанным ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Трек уже опубликован */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID трека, которому загружается обложка */\n        trackId: string\n      }\n      cookie?: never\n    }\n    /** @description Файл изображения.<br/>\n     *                       • Имя поля — <code>cover</code><br/>\n     *                       • Допустимые MIME-типы — <code>image/jpeg</code>, <code>image/png</code>, <code>image/gif</code><br/>\n     *                       • Максимальный размер — <code>100 KB</code> */\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** Format: binary */\n          cover: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Успешная загрузка обложки */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetImagesOutput']\n        }\n      }\n      /** @description BadRequest: Неверный файл или превышен размер */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нельзя загружать обложку к чужому треку */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Обложка удалена */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Удаление обложки трека другого пользователя запрещена */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackMp3: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @example My cool track */\n          title: string\n          /** Format: binary */\n          file: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Трек успешно создан */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackOutput']\n        }\n      }\n      /** @description BadRequest: Неверный формат файла или превышен размер */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description InternalServerError: Ошибка при сохранении файла или трека */\n      500: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_createArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateArtistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Исполнитель успешно создан */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetArtistOutput']\n        }\n      }\n      /** @description BadRequest: Ошибка валидации или неверный ввод */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Лимит в 100 артистов на пользователя исчерпан */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Исполнитель с таким именем уже существует */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_searchArtist: {\n    parameters: {\n      query: {\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Список исполнителей найден по подстроке */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetArtistOutput'][]\n        }\n      }\n    }\n  }\n  ArtistsController_deleteArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Исполнитель успешно удалён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Другой юзер создал данного артиста или данный артист прикреплён к трекам */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Исполнитель с таким ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_OauthRedirect: {\n    parameters: {\n      query?: {\n        /** @description The callback URL to redirect after grand access,\n         *          https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid */\n        callbackUrl?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Редирект выполнен */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_login: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LoginRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Успешно получена пара токенов */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description BadRequest: Неверный формат запроса или отсутствуют обязательные параметры */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['BadRequestException']\n        }\n      }\n      /** @description Unauthorized: Код недействителен, истёк или не передан, или не совпадает redirectUri */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_refresh: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['RefreshRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Успешное обновление пары токенов */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Unauthorized: Refresh-token недействителен, истёк или не передан */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_logout: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LogoutRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: refresh токен деактивирован, при этом access-токен остаётся ещё валидным. */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_getMe: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Успешное получение информации о пользователе */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMeOutput']\n        }\n      }\n      /** @description Unauthorized: access токен отсутствует или недействителен */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_createTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateTagRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Тег успешно создан */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput']\n        }\n      }\n      /** @description BadRequest: Ошибка валидации */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Лимит в 100 тегов на пользователя исчерпан */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Тег с таким именем уже существует */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_searchTags: {\n    parameters: {\n      query: {\n        /** @description Подстрока для поиска тегов (по нормализованному имени) */\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Список подходящих тегов */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput'][]\n        }\n      }\n      /** @description BadRequest: Некорректный поисковый запрос */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_deleteTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID удаляемого тега */\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Тег успешно удалён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Другой юзер создал данный тег или данй тег прикреплён к трекам и плейлистам */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Тег с указанным ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/api/socket.ts",
    "content": "import { io, Socket } from 'socket.io-client'\n\nlet sharedSocket: Socket | null = null\n\nconst BASE = import.meta.env.VITE_BASE_URL.replace('api/1.0', '')\nconst PATH = '/api/1.0/ws'\n\n/** создать новый socket с (или без) токена */\nfunction createSocket(token: string | null): Socket {\n  return io(BASE, {\n    path: PATH,\n    transports: ['websocket'],\n    ...(token ? { auth: { token } } : {}),\n  })\n}\n\n/** получить singleton-сокет (гость, если токена нет) */\nexport function getSharedSocket(token: string | null): Socket {\n  if (!sharedSocket) sharedSocket = createSocket(token)\n  return sharedSocket\n}\n\n/**\n * вызвать после логина / логаута.\n * – token === null  → переключаемся на гостевой сокет\n * – token === 'XXX' → подключаемся авторизованно\n */\nexport function resetSocketWithToken(token: string | null): Socket {\n  // аккуратно рвём старое соединение\n  if (sharedSocket) {\n    sharedSocket.disconnect()\n  }\n\n  sharedSocket = createSocket(token)\n  return sharedSocket\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/config/api.config.ts",
    "content": "export const apiBaseUrl = import.meta.env.VITE_BASE_URL\nexport const apiKey = import.meta.env.VITE_API_KEY\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/db/localstorage-keys.ts",
    "content": "export const localStorageKeys = {\n  refreshToken: 'musicfun-refresh-token',\n  accessToken: 'musicfun-access-token',\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/routes/routes.ts",
    "content": "export const ROUTES = {\n  main: '/',\n  myPlaylists: '/my-playlists',\n} as const\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/ui/header/header.component.tsx",
    "content": "import { Link } from '@tanstack/react-router'\nimport type { ReactNode } from 'react'\n\nimport styles from './header.module.css'\n\ntype Props = {\n  renderAccountBar: () => ReactNode\n}\n\nexport const Header = ({ renderAccountBar }: Props) => (\n  <header className={styles.header}>\n    <div className={styles.container}>\n      <div className={styles.linksBlock}>\n        <Link to=\"/\">Main</Link>\n        <Link to=\"/playlists-with-filters\">Playlists</Link>\n      </div>\n\n      <div>{renderAccountBar()}</div>\n    </div>\n  </header>\n)\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/ui/header/header.module.css",
    "content": ".header {\n  border-bottom: #aaaaaa 1px solid;\n  padding-bottom: 10px;\n}\n\n.container {\n  max-width: 900px;\n  margin: 0 auto;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n}\n\n.linksBlock {\n  display: flex;\n  gap: 10px;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/ui/pagination/pagination-nav/pagination-nav.module.css",
    "content": ".pagination {\n  display: flex;\n  gap: 8px;\n  justify-content: center;\n}\n\n.pageButton {\n  padding: 4px 10px;\n  background: transparent;\n  border: 1px solid #aaa;\n  border-radius: 4px;\n  cursor: pointer;\n  font-weight: normal;\n  transition:\n    background 0.2s,\n    color 0.2s;\n  color: white;\n}\n\n.pageButtonActive {\n  background: #ececec;\n  font-weight: bold;\n  cursor: default;\n  color: black;\n}\n\n.ellipsis {\n  padding: 4px 10px;\n  user-select: none;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/ui/pagination/pagination-nav/pagination-nav.tsx",
    "content": "import { getPaginationPages } from '../utils/get-pagination-pages.ts'\nimport s from './pagination-nav.module.css'\n\ntype Props = {\n  current: number\n  pagesCount: number\n  onChange: (page: number) => void\n}\n\nconst SIBLING_COUNT = 1\n\nexport const PaginationNav = ({ current, pagesCount, onChange }: Props) => {\n  const pages = getPaginationPages(current, pagesCount, SIBLING_COUNT)\n\n  return (\n    <div className={s.pagination}>\n      {pages.map((item, idx) =>\n        item === '...' ? (\n          <span className={s.ellipsis} key={`ellipsis-${idx}`}>\n            ...\n          </span>\n        ) : (\n          <button\n            key={item}\n            className={item === current ? `${s.pageButton} ${s.pageButtonActive}` : s.pageButton}\n            onClick={() => item !== current && onChange(Number(item))}\n            disabled={item === current}\n            type=\"button\">\n            {item}\n          </button>\n        )\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/ui/pagination/pagination.module.css",
    "content": ".container {\n  display: flex;\n  align-content: center;\n  align-items: center;\n  margin: 0 auto;\n  gap: 40px;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/ui/pagination/pagination.tsx",
    "content": "import s from './Pagination.module.css'\nimport { PaginationNav } from './pagination-nav/pagination-nav.tsx'\n\ntype Props = {\n  current: number\n  pagesCount: number\n  changePageNumber: (page: number) => void\n  isFetching: boolean\n}\n\nexport const Pagination = ({ current, pagesCount, changePageNumber, isFetching }: Props) => {\n  return (\n    <div className={s.container}>\n      <PaginationNav current={current} pagesCount={pagesCount} onChange={changePageNumber} />{' '}\n      {isFetching && '⌛️'}\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/shared/ui/pagination/utils/get-pagination-pages.ts",
    "content": "/**\n * Генерирует массив страниц для отображения пагинации с учётом троеточий\n */\nexport const getPaginationPages = (\n  current: number,\n  pagesCount: number,\n  siblingCount: number\n): (number | '...')[] => {\n  if (pagesCount <= 1) return []\n\n  const pages: (number | '...')[] = []\n\n  // Границы диапазона вокруг текущей страницы\n  const leftSibling = Math.max(2, current - siblingCount)\n  const rightSibling = Math.min(pagesCount - 1, current + siblingCount)\n\n  // Всегда показываем первую страницу\n  pages.push(1)\n\n  // Троеточие слева\n  if (leftSibling > 2) {\n    pages.push('...')\n  }\n\n  // Соседние страницы вокруг текущей\n  for (let page = leftSibling; page <= rightSibling; page++) {\n    pages.push(page)\n  }\n\n  // Троеточие справа\n  if (rightSibling < pagesCount - 1) {\n    pages.push('...')\n  }\n\n  // Всегда показываем последнюю страницу (если больше одной)\n  if (pagesCount > 1) {\n    pages.push(pagesCount)\n  }\n\n  return pages\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true,\n    \"baseUrl\": \".\", // allow non-relative imports\n    \"paths\": {\n      \"@/*\": [\"src/*\"] // map “@/foo” → “src/foo”\n    }\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\", // or \"NodeNext\"\n    \"moduleResolution\": \"Bundler\", // or \"NodeNext\"\n    \"noUncheckedIndexedAccess\": true\n  },\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/tsr.config.json",
    "content": "{\n  \"routesDirectory\": \"./src/app/routes\",\n  \"generatedRouteTree\": \"./src/app/routes/routeTree.gen.ts\"\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-orval-small-example/vite.config.ts",
    "content": "import tanstackRouter from '@tanstack/router-plugin/vite'\nimport react from '@vitejs/plugin-react'\nimport path from 'path'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [\n    tanstackRouter({\n      target: 'react',\n      autoCodeSplitting: true,\n    }),\n    react(),\n  ],\n  resolve: {\n    alias: {\n      // “@” will map to the /src directory\n      '@': path.resolve(__dirname, 'src'),\n    },\n  },\n  server: {\n    host: true, // ← or '0.0.0.0'\n    port: 5174,\n    strictPort: true,\n    allowedHosts: [\n      'domain.prod', // <-- your custom host\n      'localhost', // (optional) keep localhost too\n    ],\n  },\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\nrouteTree.gen.ts\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/README.md",
    "content": "https://chatgpt.com/c/6867c378-f864-8006-85a1-98da24df147c?model=o4-mini-high\n\n## Create Vite React Project\n\npnpm create vite\nhttps://vite.dev/guide/\n\n## Tanstack Installation\n\nhttps://tanstack.com/query/latest/docs/framework/react/installation\n\npnpm add @tanstack/react-query\n\n## clear app.tsx\n\n```tsx\nfunction App() {\n  return <>Hello</>\n}\n\nexport default App\n```\n\n## remove StrictMode\n\nin main.tsx\n\n## activate eslint on save\n\n## run project\n\npnpm dev\n\n## generate api layer\n\nhttps://openapi-ts.dev/introduction\n\npnpm i -D openapi-typescript typescript\n\nAnd in your tsconfig.json, to load the types properly:\n\"compilerOptions\": {\n\"module\": \"ESNext\", // or \"NodeNext\"\n\"moduleResolution\": \"Bundler\" // or \"NodeNext\"\n}\n\nHighly recommended\n\nAlso adding the following can boost type safety:\n\ntsconfig.json\n\n{\n\"compilerOptions\": {\n\"noUncheckedIndexedAccess\": true\n}\n}\n\n## install openapi-fetch\n\npnpm i openapi-fetch\n\n## add script and generate api\n\n\"generate:api\": \"pnpm dlx openapi-typescript https://spotifun.it-incubator.app/api-json -o ./src/shared/api/schema.ts\"\n\n# add env files\n\n.env\nVITE_BASE_URL=https://spotifun.it-incubator.app/api/1.0\nVITE_API_KEY=\nVITE_CURRENT_DOMAIN=http://localhost:5174\n\n.env.local\nVITE_API_KEY=72c3121c-c679-4c0e-9131-2d3f35e6a3bd\n\n## add client.tsx\n\n```typescript\nimport createClient, { type Middleware } from 'openapi-fetch'\nimport type { paths } from './schema.ts'\n\nconst config = {\n  baseURL: null as string | null,\n  apiKey: null as string | null,\n  getAccessToken: null as (() => Promise<string | null>) | null,\n  saveAccessToken: null as (() => Promise<void>) | null,\n  getRefreshToken: null as (() => Promise<string | null>) | null,\n  saveRefreshToken: null as (() => Promise<void>) | null,\n}\n\nexport const setClientConfig = (newConfig: Partial<typeof config>) => {\n  Object.assign(config, newConfig)\n  _client = undefined // пере-инициализируем\n}\n\nconst authMiddleware: Middleware = {\n  /* ---------- REQUEST -------------------------------------------------- */\n  async onRequest({ request }) {\n    request.headers.set('API-KEY', config.apiKey!)\n\n    const token = await config.getAccessToken?.()\n    if (token) request.headers.set('Authorization', `Bearer ${token}`)\n\n    return request\n  },\n}\n\nlet _client: ReturnType<typeof createClient<paths>> | undefined\n\nexport const getClient = () => {\n  if (_client) return _client\n\n  if (!config.baseURL || !config.apiKey) {\n    console.error('call setClientConfig to setup api')\n    throw new Error('call setClientConfig to setup api')\n  }\n\n  const client = createClient<paths>({ baseUrl: config.baseURL })\n  client.use(authMiddleware)\n  _client = client\n  return _client\n}\n```\n\n## setup client in main.tsx\n\n```tsx\n\n```\n\n## const queryClient = new QueryClient()\n\n```tsx\nconst queryClient = new QueryClient()\n\nfunction App() {\n  return (\n    // Provide the client to your App\n    <QueryClientProvider client={queryClient}>\n      <Playlists />\n    </QueryClientProvider>\n  )\n}\n```\n\n## add page Playlists\n\nsrc/pages/playlists/playlists.tsx\n\n```tsx\n\n```\n\n## devtool\n\npnpm i @tanstack/react-query-devtools\n\n## install tanstack router\n\npnpm add @tanstack/react-router\npnpm install -D @tanstack/router-plugin\n\nhttps://tanstack.com/router/latest/docs/framework/react/quick-start\n\n```typescript\ntanstackRouter({\n  target: 'react',\n  autoCodeSplitting: true,\n})\n```\n\n1. при добавляении 2 страницы.. обратить внимание.. что нет рефетчей..\n\nhttps://miro.com/app/board/uXjVIhdj_Vw=/?share_link_id=990510541124\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport pluginQuery from '@tanstack/eslint-plugin-query'\nimport { globalIgnores } from 'eslint/config'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config([\n  ...pluginQuery.configs['flat/recommended'],\n  globalIgnores(['dist']),\n  {\n    plugins: {\n      '@tanstack/query': pluginQuery,\n    },\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs['recommended-latest'],\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite + React + TS</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/app/entrypoint/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/package.json",
    "content": "{\n  \"name\": \"tanstack-query-musicfun-small-example\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\",\n    \"generate:api\": \"pnpm dlx openapi-typescript https://musicfun.it-incubator.app/api-json --root-types -o ./src/shared/api/schema.ts\",\n    \"generate:async-api\": \"pnpm asyncapi generate models typescript https://musicfun.it-incubator.app/async-api-json  -o ./src/shared/async-api/schema.ts --tsEnumType union --tsModelType interface\",\n    \"generate:async-api:dimych\": \"pnpm asyncapi generate models typescript http://localhost:9001/async-api-json -o ./src/shared/async-api --tsModelType interface --tsEnumType union --tsRawPropertyNames\",\n    \"generate:api:dimych\": \"pnpm dlx openapi-typescript http://localhost:9001/api-json -o ./src/shared/api/schema.ts\"\n  },\n  \"dependencies\": {\n    \"@tanstack/react-query\": \"^5.81.5\",\n    \"@tanstack/react-query-devtools\": \"^5.81.5\",\n    \"@tanstack/react-router\": \"^1.124.0\",\n    \"openapi-fetch\": \"^0.14.0\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"react-hook-form\": \"^7.60.0\",\n    \"react-toastify\": \"11.0.5\",\n    \"socket.io-client\": \"4.8.1\"\n  },\n  \"devDependencies\": {\n    \"@asyncapi/cli\": \"^3.2.0\",\n    \"@eslint/js\": \"^9.29.0\",\n    \"@tanstack/eslint-plugin-query\": \"^5.81.2\",\n    \"@tanstack/router-plugin\": \"^1.124.0\",\n    \"@types/react\": \"^19.1.8\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@vitejs/plugin-react\": \"^4.5.2\",\n    \"eslint\": \"^9.29.0\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.20\",\n    \"globals\": \"^16.2.0\",\n    \"openapi-typescript\": \"^7.8.0\",\n    \"path\": \"^0.12.7\",\n    \"typescript\": \"~5.8.3\",\n    \"typescript-eslint\": \"^8.34.1\",\n    \"vite\": \"^7.0.0\"\n  },\n  \"packageManager\": \"pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac\"\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/entrypoint/main.tsx",
    "content": "import '../styles/reset.css'\nimport '../styles/index.css'\n\nimport { createRouter, RouterProvider } from '@tanstack/react-router'\nimport { createRoot } from 'react-dom/client'\n\nimport { routeTree } from '@/app/routes/routeTree.gen.ts'\nimport { setClientConfig } from '@/shared/api/client.ts'\nimport { apiBaseUrl, apiKey } from '@/shared/config/api.config.ts'\nimport { localStorageKeys } from '@/shared/db/localstorage-keys.ts'\n\nconst router = createRouter({ routeTree })\n\n// Register the router instance for type safety\ndeclare module '@tanstack/react-router' {\n  interface Register {\n    router: typeof router\n  }\n}\n\nsetClientConfig({\n  baseURL: apiBaseUrl,\n  apiKey: apiKey,\n  getAccessToken: async () => localStorage.getItem(localStorageKeys.accessToken),\n  getRefreshToken: async () => localStorage.getItem(localStorageKeys.refreshToken),\n  saveAccessToken: async (token) =>\n    token\n      ? localStorage.setItem(localStorageKeys.accessToken, token)\n      : localStorage.removeItem(localStorageKeys.accessToken),\n  saveRefreshToken: async (token) =>\n    token\n      ? localStorage.setItem(localStorageKeys.refreshToken, token)\n      : localStorage.removeItem(localStorageKeys.refreshToken),\n})\n\ncreateRoot(document.getElementById('root')!).render(<RouterProvider router={router} />)\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/layouts/root-layout.module.css",
    "content": ".container {\n  padding-top: 10px;\n  max-width: 900px;\n  margin: 0 auto;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/layouts/root-layout.tsx",
    "content": "import { QueryClientProvider } from '@tanstack/react-query'\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools'\nimport { Outlet } from '@tanstack/react-router'\nimport { ToastContainer } from 'react-toastify'\n\nimport styles from '@/app/layouts/root-layout.module.css'\nimport { WebSocketProvider } from '@/app/providers/web-socket-provider.tsx'\nimport { queryClient } from '@/app/query-client/query-client.tsx'\nimport { AccountBar } from '@/features/auth'\nimport { Header } from '@/shared/ui/header/header.component.tsx'\n\nexport function RootLayout() {\n  return (\n    <>\n      <QueryClientProvider client={queryClient}>\n        <WebSocketProvider>\n          <Header renderAccountBar={() => <AccountBar />} />\n          <div className={styles.container}>\n            <Outlet />\n          </div>\n          <ReactQueryDevtools initialIsOpen={false} buttonPosition={'bottom-left'} />\n          <ToastContainer />\n        </WebSocketProvider>\n      </QueryClientProvider>\n    </>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/providers/web-socket-provider.tsx",
    "content": "import type { ReactNode } from 'react'\n\nexport function WebSocketProvider({ children }: { children: ReactNode }) {\n  return <>{children}</>\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/query-client/query-client.tsx",
    "content": "import { MutationCache, QueryClient } from '@tanstack/react-query'\n\nimport { mutationGlobalErrorHandler } from '@/shared/api/query-error-handler-for-rhf-factory.ts'\n\nexport type MutationMeta = {\n  /**\n   * Если 'off' — глобальный обработчик ошибок пропускаем,\n   * если 'on' (или нет поля) — вызываем.\n   */\n  globalErrorHandler?: 'on' | 'off'\n}\n\ndeclare module '@tanstack/react-query' {\n  interface Register {\n    /**\n     * Тип для поля `meta` в useMutation(...)\n     */\n    mutationMeta: MutationMeta\n  }\n}\n\nexport const queryClient = new QueryClient({\n  mutationCache: new MutationCache({\n    onError: mutationGlobalErrorHandler, // 🔹 вызывается ВСЕГДА\n  }),\n  defaultOptions: {\n    queries: {\n      refetchOnWindowFocus: false,\n      refetchOnMount: false,\n      staleTime: Infinity, //5000,\n      //gcTime: 10000 // если нет подписчиков - удалить всё нафик...\n    },\n  },\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/routes/__root.tsx",
    "content": "import 'react-toastify/dist/ReactToastify.css'\n\nimport { createRootRoute } from '@tanstack/react-router'\n\nimport { RootLayout } from '@/app/layouts/root-layout.tsx'\n\nexport const Route = createRootRoute({\n  component: RootLayout,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/routes/index.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { Playlists } from '../../features/playlists/list/playlists.tsx'\n\nexport const Route = createFileRoute('/')({\n  component: () => <Playlists filtersEnabled={false} />,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/routes/my-playlists.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { MyPlaylistsPage } from '@/pages/playlists/ui/my-playlists/my-playlists-page.tsx'\nimport { ROUTES } from '@/shared/routes/routes.ts'\n\nexport const Route = createFileRoute(ROUTES.myPlaylists)({\n  component: MyPlaylistsPage,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/routes/oauth/callback.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { OauthCallbackPage } from '@/pages/auth'\n\nexport const Route = createFileRoute('/oauth/callback')({\n  component: OauthCallbackPage,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/routes/playlists-with-filters.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { PlaylistsWithFiltersPage } from '@/pages/playlists/ui/playlists-with-filters-page.tsx'\n\nexport const Route = createFileRoute('/playlists-with-filters')({\n  component: PlaylistsWithFiltersPage,\n})\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/styles/index.css",
    "content": "body {\n  font-family:\n    system-ui,\n    -apple-system,\n    BlinkMacSystemFont,\n    'Segoe UI',\n    Roboto,\n    'Helvetica Neue',\n    Arial,\n    sans-serif;\n  background: #060707;\n\n  color: #9c9c9c;\n}\na {\n  color: #9c9c9c;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/app/styles/reset.css",
    "content": "/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Prevent font size inflation */\nhtml {\n  -moz-text-size-adjust: none;\n  -webkit-text-size-adjust: none;\n  text-size-adjust: none;\n}\n\n/* Remove default margin in favour of better control in authored CSS */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nfigure,\nblockquote,\ndl,\ndd {\n  margin-block-end: 0;\n}\n\n/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */\nul,\nol {\n  list-style: none;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  line-height: 1.5;\n}\n\n/* Set shorter line heights on headings and interactive elements */\nh1,\nh2,\nh3,\nh4,\nbutton,\ninput,\nlabel {\n  line-height: 1.1;\n}\n\n/* Balance text wrapping on headings */\nh1,\nh2,\nh3,\nh4 {\n  text-wrap: balance;\n}\n\n/* A elements that don't have a class get default styles */\na:not([class]) {\n  text-decoration-skip-ink: auto;\n  color: currentColor;\n}\n\n/* Make images easier to work with */\nimg,\npicture {\n  max-width: 100%;\n  display: block;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n/* Make sure textareas without a rows attribute are not tiny */\ntextarea:not([rows]) {\n  min-height: 10em;\n}\n\n/* Anything that has been anchored to should have extra scroll margin */\n:target {\n  scroll-margin-block: 5ex;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/api/auth-api.types.ts",
    "content": "import { getClientConfig } from '@/shared/api/client.ts'\nimport type { SchemaLoginRequestPayload } from '@/shared/api/schema.ts'\n\nexport type LoginRequestPayload = SchemaLoginRequestPayload\n\nexport const getOauthRedirectUrl = (redirectUrl: string) =>\n  getClientConfig().baseURL + `/auth/oauth-redirect?callbackUrl=${redirectUrl}`\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/api/use-login.mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '../../../shared/api/client.ts'\nimport { localStorageKeys } from '../../../shared/db/localstorage-keys.ts'\nimport type { LoginRequestPayload } from './auth-api.types.ts'\n\nexport const useLoginMutation = () => {\n  const qc = useQueryClient()\n  return useMutation({\n    mutationFn: (payload: LoginRequestPayload) => {\n      return getClient().POST('/auth/login', {\n        body: payload,\n      })\n    },\n    onSuccess: async (data) => {\n      localStorage.setItem(localStorageKeys.refreshToken, data.data!.refreshToken)\n      localStorage.setItem(localStorageKeys.accessToken, data.data!.accessToken)\n      await qc.invalidateQueries({ queryKey: ['auth'] })\n\n      await qc.invalidateQueries()\n    },\n  })\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/api/use-logout.mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { getClient } from '../../../shared/api/client.ts'\nimport { localStorageKeys } from '../../../shared/db/localstorage-keys.ts'\n\nexport const useLogoutMutation = () => {\n  const qc = useQueryClient()\n  return useMutation({\n    mutationFn: () => {\n      return getClient().POST('/auth/logout', {\n        body: {\n          refreshToken: localStorage.getItem(localStorageKeys.refreshToken)!,\n        },\n      })\n    },\n    onSuccess: async () => {\n      localStorage.removeItem(localStorageKeys.accessToken)\n      localStorage.removeItem(localStorageKeys.refreshToken)\n      qc.resetQueries({ queryKey: ['auth'] }) // resetQueries переводит query в изначальное состояние и уведомляет подписчиков — компонент получит data = undefined.\n      //qc.invalidateQueries({ queryKey: [authKey] }) // invalidateQueries заставит его немедленно перефетчиться без токена ⇒ получите 401 ⇒ data станет undefined / error.\n    },\n  })\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/api/use-me.query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '@/shared/api/client.ts'\nimport { requestWrapper } from '@/shared/api/request-wrapper.ts'\n\nexport const useMeQuery = () => {\n  return useQuery({\n    queryKey: ['auth', 'me'],\n    queryFn: () => requestWrapper(getClient().GET('/auth/me')),\n    retry: false,\n  })\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/index.tsx",
    "content": "export { AccountBar } from './ui/account-bar.tsx'\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/ui/account-bar.module.css",
    "content": ".meInfoContainer {\n  display: flex;\n  gap: 10px;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/ui/account-bar.tsx",
    "content": "import { CurrentUser } from '@/features/auth/ui/current-user/current-user.tsx'\nimport { LoginButton } from '@/features/auth/ui/login-button/login-button.tsx'\n\nimport { useMeQuery } from '../api/use-me.query.ts'\n\nexport const AccountBar = () => {\n  const query = useMeQuery()\n\n  return (\n    <div>\n      {!query.data && <LoginButton />}\n      {query.data && <CurrentUser />}\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/ui/current-user/current-user.tsx",
    "content": "import { Link } from '@tanstack/react-router'\n\nimport { LogoutButton } from '@/features/auth/ui/logout-button/logout-button.tsx'\n\nimport { useMeQuery } from '../../api/use-me.query.ts'\nimport styles from '../account-bar.module.css'\n\nexport const CurrentUser = () => {\n  const query = useMeQuery()\n\n  return (\n    <div className={styles.meInfoContainer}>\n      <Link to=\"/my-playlists\" activeOptions={{ exact: true }}>\n        {query.data!.login}\n      </Link>\n\n      <LogoutButton />\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/ui/login-button/login-button.tsx",
    "content": "import { useLogin } from '@/features/auth/ui/login-button/use-login.tsx'\n\nexport const LoginButton = () => {\n  const { login: handleLoginClick } = useLogin()\n\n  return <button onClick={handleLoginClick}>Login with APIHUB</button>\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/ui/login-button/use-login.tsx",
    "content": "import { getOauthRedirectUrl } from '../../api/auth-api.types.ts'\nimport { useLoginMutation } from '../../api/use-login.mutation.ts'\n\nconst currentDomain = import.meta.env.VITE_CURRENT_DOMAIN\n\nexport const useLogin = () => {\n  const { mutate } = useLoginMutation()\n\n  function login() {\n    const redirectUri = currentDomain + '/oauth/callback' // todo: to config\n    const url = getOauthRedirectUrl(redirectUri)\n    window.open(url, 'oauthPopup', 'width=500,height=600')\n\n    const handleOauthMessage = async (event: MessageEvent) => {\n      if (event.origin !== currentDomain) {\n        return\n      }\n\n      const { code } = event.data\n      if (code) {\n        console.log('✅ code received:', code)\n        window.removeEventListener('message', handleOauthMessage)\n        mutate({ code, accessTokenTTL: '10s', redirectUri, rememberMe: true })\n      }\n    }\n\n    window.addEventListener('message', handleOauthMessage)\n  }\n\n  return { login }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/ui/logout-button/logout-button.tsx",
    "content": "import { useLogout } from '@/features/auth/ui/logout-button/use-logout.ts'\n\nexport const LogoutButton = () => {\n  const { logout: handleLogoutClick } = useLogout()\n\n  return <button onClick={handleLogoutClick}>Logout</button>\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/auth/ui/logout-button/use-logout.ts",
    "content": "import { useLogoutMutation } from '@/features/auth/api/use-logout.mutation.ts'\n\nexport const useLogout = () => {\n  const { mutate } = useLogoutMutation()\n\n  const logout = () => {\n    mutate()\n  }\n\n  return { logout }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/add-playlist-form/add-playlist-form.module.css",
    "content": ".form {\n  border-bottom: 1px solid grey;\n  padding: 10px;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/add-playlist-form/add-playlist-form.tsx",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { type SubmitHandler, useForm } from 'react-hook-form'\n\nimport type { components } from '@/shared/api'\nimport { getClient } from '@/shared/api'\nimport { requestWrapper } from '@/shared/api/request-wrapper.ts'\n\nimport styles from './add-playlist-form.module.css'\n\nexport type CreatePlaylistRequestPayload = components['schemas']['CreatePlaylistRequestPayload']\n\nexport const AddPlaylistForm = () => {\n  const queryClient = useQueryClient()\n\n  const {\n    register,\n    handleSubmit,\n    reset,\n    formState: { isSubmitting },\n  } = useForm<CreatePlaylistRequestPayload>({\n    defaultValues: { title: '', description: '' },\n  })\n\n  const { mutate } = useMutation({\n    mutationFn: (body: CreatePlaylistRequestPayload) =>\n      requestWrapper(getClient().POST('/playlists', { body })),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['playlists'] })\n      reset()\n    },\n    onError: (err: unknown) => {\n      console.error(err)\n      alert(JSON.stringify(err))\n      throw err\n    },\n    meta: { globalErrorHandler: 'on' },\n  })\n\n  const onSubmit: SubmitHandler<CreatePlaylistRequestPayload> = (data) => {\n    mutate(data)\n  }\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)} className={styles.form}>\n      <h2>Add a New Playlist</h2>\n\n      <p>\n        <input\n          {...register('title', { required: true })}\n          placeholder=\"Title\"\n          disabled={isSubmitting}\n        />\n      </p>\n\n      <div>\n        <input {...register('description')} placeholder=\"Description\" disabled={isSubmitting} />\n      </div>\n\n      <button type=\"submit\" disabled={isSubmitting}>\n        {isSubmitting ? 'Creating…' : 'Create Playlist'}\n      </button>\n    </form>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/api/use-playlists-query.tsx",
    "content": "import { keepPreviousData, useQuery } from '@tanstack/react-query'\n\nimport { getClient } from '../../../shared/api/client.ts'\nimport { requestWrapper } from '../../../shared/api/request-wrapper.ts'\nimport type { SchemaGetPlaylistsRequestPayload } from '../../../shared/api/schema.ts'\n\nexport const playlistListKey = (p: Partial<SchemaGetPlaylistsRequestPayload> = {}) => {\n  const {\n    pageNumber = 1,\n    pageSize = 20,\n    search = '',\n    sortBy = 'publishedAt',\n    sortDirection = 'desc',\n    tagsIds = [],\n    userId = null,\n    trackId = null,\n  } = p\n\n  return [\n    'playlists',\n    {\n      pageNumber,\n      pageSize,\n      search,\n      sortBy,\n      sortDirection,\n      tagsIds: [...tagsIds].sort(),\n      userId,\n      trackId,\n    } as SchemaGetPlaylistsRequestPayload,\n  ] as const // даёт key-tuple с readonly типами\n}\n\nexport function usePlaylistsQuery(search: string, pageNumber: number, userId: string | undefined) {\n  const query = useQuery({\n    queryKey: playlistListKey({\n      search,\n      pageNumber,\n      userId,\n    }),\n    queryFn: () => {\n      return requestWrapper(\n        getClient().GET('/playlists', {\n          params: {\n            query: {\n              search: search && undefined,\n              pageNumber,\n              pageSize: 5,\n              userId,\n            },\n          },\n        })\n      )\n    },\n    placeholderData: keepPreviousData,\n  })\n  return query\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/edit-playlist-form/edit-playlist-form.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useEffect } from 'react'\nimport { type SubmitHandler, useForm } from 'react-hook-form'\n\nimport { getClient } from '../../../shared/api/client'\nimport { queryErrorHandlerForRHFFactory } from '../../../shared/api/query-error-handler-for-rhf-factory.ts'\nimport { requestWrapper } from '../../../shared/api/request-wrapper.ts'\nimport type { components } from '../../../shared/api/schema'\n\ntype Props = {\n  classNames: string\n  playlistId: string | null\n  onCancelEditing: () => void\n}\n\ntype UpdatePlaylistRequestPayload = components['schemas']['UpdatePlaylistRequestPayload']\n\nexport const EditPlaylistForm = ({ playlistId, onCancelEditing, classNames }: Props) => {\n  const queryClient = useQueryClient()\n\n  /* 1. Загружаем детали плейлиста */\n  const { data: playlistResp, isPending: isPlaylistPending } = useQuery({\n    queryKey: ['playlists', 'details', playlistId],\n    queryFn: ({ signal }) =>\n      getClient().GET('/playlists/{playlistId}', {\n        params: { path: { playlistId: playlistId! } },\n        signal,\n      }),\n    enabled: Boolean(playlistId),\n  })\n\n  /* 2. useForm */\n  const {\n    register,\n    handleSubmit,\n    reset,\n    setError,\n    formState: { isSubmitting, errors },\n  } = useForm<UpdatePlaylistRequestPayload>({\n    defaultValues: { title: '', description: '' }, // дефолты\n  })\n\n  /* 3. Сброс/инициализация формы, когда данные загрузились */\n  useEffect(() => {\n    if (playlistResp?.data) {\n      const { title = '', description = '' } = playlistResp.data.data.attributes\n      reset({ title, description })\n    }\n  }, [playlistResp, reset])\n\n  /* 4. Мутация «обновить плейлист» */\n  const { mutate, isPending } = useMutation({\n    mutationFn: (body: UpdatePlaylistRequestPayload) =>\n      requestWrapper(\n        getClient().PUT('/playlists/{playlistId}', {\n          body: { ...body, tagIds: [] },\n          params: { path: { playlistId: playlistId! } },\n        })\n      ),\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: ['playlists'],\n      })\n      onCancelEditing?.()\n    },\n    onError: queryErrorHandlerForRHFFactory({ setError }),\n  })\n\n  /* 5. Сабмит формы */\n  const onSubmit: SubmitHandler<UpdatePlaylistRequestPayload> = (values) => {\n    if (!playlistId) return\n    mutate(values)\n  }\n\n  if (!playlistId) return null\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)} className={classNames}>\n      <h2>Редактировать плейлист</h2>\n\n      <p>\n        <label>\n          <input\n            {...register('title')}\n            placeholder=\"Title\"\n            disabled={isPending || isPlaylistPending || isSubmitting}\n          />\n        </label>\n      </p>\n      {errors.title && <p>{errors.title.message}</p>}\n      <p>\n        <label>\n          <textarea\n            {...register('description')}\n            placeholder=\"Description\"\n            disabled={isPending || isPlaylistPending || isSubmitting}\n          />\n        </label>\n      </p>\n      {errors.description && <p>{errors.description.message}</p>}\n\n      <button type=\"submit\" disabled={isPending || isPlaylistPending || isSubmitting}>\n        {isPending ? 'Сохраняем…' : 'Сохранить'}\n      </button>\n\n      {errors.root?.server && <p>{errors.root.server.message}</p>}\n\n      <button onClick={() => onCancelEditing?.()}>Cancel</button>\n    </form>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/list/paginated-playlists.module.css",
    "content": ".cardBox {\n  width: 300px;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: start;\n}\n\n.title {\n  word-break: break-all;\n}\n\n.deletePlaylistButton {\n  all: unset;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/list/paginated-playlists.tsx",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { useEffect, useState } from 'react'\n\nimport { Pagination } from '@/shared/ui/pagination/pagination.tsx'\n\nimport { getClient } from '../../../shared/api/client.ts'\nimport type {\n  SchemaGetPlaylistOutput,\n  SchemaGetPlaylistsOutput,\n} from '../../../shared/api/schema.ts'\nimport { getSharedSocket } from '../../../shared/api/socket.ts'\nimport { useMeQuery } from '../../auth/api/use-me.query.ts'\nimport { playlistListKey, usePlaylistsQuery } from '../api/use-playlists-query.tsx'\nimport { PlaylistCover } from '../playlist-cover/playlist-cover.tsx'\nimport styles from './paginated-playlists.module.css'\n\ntype Props = {\n  classNames?: string\n  userId?: string\n  onPlaylistSelected?: (playlistId: string) => void\n}\n\nexport type PlaylistCreatedEventPayload = SchemaGetPlaylistOutput\n\nexport const PlaylistCreatedEventName = 'tracks.playlist-created'\nexport type PlaylistCreatedEvent = {\n  type: typeof PlaylistCreatedEventName\n  payload: PlaylistCreatedEventPayload\n}\n\nexport const PaginatedPlaylists = ({ userId, onPlaylistSelected, classNames }: Props) => {\n  const [search, setSearch] = useState('')\n  const [pageNumber, setPageNumber] = useState(1)\n\n  const { data: meData } = useMeQuery()\n  const query = usePlaylistsQuery(search, pageNumber, userId)\n\n  const queryClient = useQueryClient()\n\n  useEffect(() => {\n    const socket = getSharedSocket(import.meta.env.VITE_AUTH_TOKEN)\n\n    socket.on(PlaylistCreatedEventName, (data: PlaylistCreatedEvent) => {\n      queryClient.setQueryData(\n        playlistListKey({ search, pageNumber: 1, userId: undefined }),\n        (oldData: SchemaGetPlaylistsOutput) => {\n          return {\n            data: [data.payload.data, ...oldData.data],\n            meta: oldData.meta,\n          } as SchemaGetPlaylistsOutput\n        }\n      )\n    })\n  }, [])\n\n  const { mutate: deletePlaylist } = useMutation({\n    mutationFn: (playlistId: string) =>\n      getClient().DELETE('/playlists/{playlistId}', {\n        params: {\n          path: {\n            playlistId,\n          },\n        },\n      }),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['playlists'] })\n    },\n    // onError: (err: unknown) =>\n    //     showErrorToast(\"Ошибка при создании плейлиста\", err),\n  })\n\n  console.log('Playlists rendered')\n\n  if (query.isPending) {\n    return <span>Loading...</span>\n  }\n\n  if (query.isError) {\n    return <span>Error: {query.error.message}</span>\n  }\n\n  return (\n    <div className={classNames}>\n      <div>\n        <input\n          value={search}\n          onChange={(e) => setSearch(e.currentTarget.value)}\n          placeholder={'search...'}\n        />\n      </div>\n      <hr />\n      <Pagination\n        current={pageNumber}\n        pagesCount={query.data!.meta.pagesCount || 0}\n        changePageNumber={setPageNumber}\n        isFetching={query.isFetching}\n      />\n\n      <ul>\n        {query.data!.data.map((playlist) => (\n          <li\n            className={styles.cardBox}\n            key={playlist.id}\n            onClick={(e) => {\n              if (e.target === e.currentTarget) {\n                onPlaylistSelected?.(playlist.id)\n              }\n            }}>\n            <div className={styles.row}>\n              <PlaylistCover\n                images={playlist.attributes.images}\n                playlistId={playlist.id}\n                editable={playlist.attributes.user.id === meData?.userId}\n              />\n              {meData?.userId === playlist.attributes.user.id && (\n                <button\n                  className={styles.deletePlaylistButton}\n                  onClick={() => deletePlaylist(playlist.id)}\n                  title={'Delete playlist'}\n                  aria-label={'Delete playlist'}>\n                  🗑️\n                </button>\n              )}\n            </div>\n\n            <h3 className={styles.title}>{playlist.attributes.title}</h3>\n\n            <hr />\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/list/playlists.tsx",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { useState } from 'react'\n\nimport { getClient } from '@/shared/api'\n\nexport const Playlists = ({ filtersEnabled = false }: { filtersEnabled: boolean }) => {\n  const [search, setSearch] = useState('')\n\n  const query = useQuery({\n    queryKey: ['playlists', search],\n    queryFn: () => {\n      return getClient().GET('/playlists', {\n        params: {\n          query: {\n            search,\n          },\n        },\n      })\n    },\n  })\n\n  console.log('Playlists rendered')\n\n  if (query.isPending) {\n    return <span>Loading...</span>\n  }\n\n  if (query.isError) {\n    return <span>Error: {query.error.message}</span>\n  }\n\n  return (\n    <div>\n      <QueryStatus query={query} />\n      {filtersEnabled && (\n        <div>\n          <input value={search} onChange={(e) => setSearch(e.currentTarget.value)} />\n        </div>\n      )}\n      <ul>\n        {query.data.data!.data.map((playlist) => (\n          <li key={playlist.id}>{playlist.attributes.title}</li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n\nfunction QueryStatus(props: any) {\n  return (\n    <div>\n      <div>status: {props.query.status}</div>\n      <div>fetchStatus: {props.query.fetchStatus}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/playlist-cover/playlist-cover.module.css",
    "content": ".container {\n  margin-bottom: 30px;\n}\n\n.cover {\n  width: 100px;\n  height: 100px;\n  object-fit: cover;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/features/playlists/playlist-cover/playlist-cover.tsx",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport type { ChangeEvent } from 'react'\n\nimport type { components } from '@/shared/api'\nimport { getClient } from '@/shared/api'\n\nimport noCover from '../../../assets/img/no-cover.png'\nimport s from './playlist-cover.module.css'\n\ntype PlaylistImagesOutputDTO = components['schemas']['PlaylistImagesOutputDTO']\n\ntype Props = {\n  editable?: boolean\n  images: PlaylistImagesOutputDTO\n  playlistId: string\n}\n\nexport const PlaylistCover = ({ images, playlistId, editable = false }: Props) => {\n  const queryClient = useQueryClient()\n  const { mutate } = useMutation({\n    mutationFn: (args: { file: File }) => {\n      const { file } = args\n      const formData = new FormData()\n      formData.append('file', file)\n      return getClient().POST('/playlists/{playlistId}/images/main', {\n        params: { path: { playlistId } },\n        body: formData as unknown as { file: string },\n      })\n    },\n    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['playlists'] }),\n    // onError: (err: unknown) => showErrorToast(\"Ошибка при загрузке изображения\", err),\n  })\n\n  const uploadCoverHandler = (event: ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0]\n    mutate({ file: file! })\n  }\n\n  const originalCover = images.main?.find((img) => img.type === 'original')\n\n  return (\n    <div className={s.container}>\n      <img\n        src={originalCover ? originalCover.url : noCover}\n        alt={'no cover image'}\n        className={s.cover}\n      />\n      {editable && (\n        <div>\n          <input\n            type=\"file\"\n            accept=\"image/jpeg,image/png,image/gif\"\n            onChange={uploadCoverHandler}\n          />\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/pages/auth/index.tsx",
    "content": "export { OauthCallbackPage } from './ui/oauth-callback-page.tsx'\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/pages/auth/ui/oauth-callback-page.tsx",
    "content": "import { useEffect } from 'react'\n\nexport function OauthCallbackPage() {\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const code = url.searchParams.get('code') // или code/state, если flow другой\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, '*') // Лучше заменить \"*\" на точный origin\n    }\n\n    window.close()\n  }, [])\n\n  return <p>Logging you in...</p>\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/pages/playlists/ui/my-playlists/my-playlists-page.module.css",
    "content": ".playlistsBox {\n  display: flex;\n  gap: 50px;\n}\n\n.playlistColumn {\n  width: 600px;\n  flex-shrink: 0;\n  padding-top: 10px;\n}\n\n.editFormColumn {\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/pages/playlists/ui/my-playlists/my-playlists-page.tsx",
    "content": "import { Navigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { useMeQuery } from '@/features/auth/api/use-me.query.ts'\nimport { AddPlaylistForm } from '@/features/playlists/add-playlist-form/add-playlist-form.tsx'\nimport { EditPlaylistForm } from '@/features/playlists/edit-playlist-form/edit-playlist-form.tsx'\nimport { PaginatedPlaylists } from '@/features/playlists/list/paginated-playlists.tsx'\n\nimport styles from './my-playlists-page.module.css'\n\nexport function MyPlaylistsPage() {\n  const { data, isLoading } = useMeQuery()\n  const [editingPlaylistId, setEditingPlaylistId] = useState<string | null>(null)\n\n  if (isLoading) return <span>loading...</span>\n\n  if (!data) {\n    // acts like React-Router’s <Navigate> / Next.js <Redirect>\n    return <Navigate to=\"/\" replace />\n  }\n\n  return (\n    <div>\n      <h3>My Playlists</h3>\n      <AddPlaylistForm />\n      <div className={styles.playlistsBox}>\n        <PaginatedPlaylists\n          onPlaylistSelected={setEditingPlaylistId}\n          userId={data.userId}\n          classNames={styles.playlistColumn}\n        />\n        <EditPlaylistForm\n          playlistId={editingPlaylistId}\n          onCancelEditing={() => setEditingPlaylistId(null)}\n          classNames={styles.editFormColumn}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/pages/playlists/ui/playlists-with-filters-page.tsx",
    "content": "import { PaginatedPlaylists } from '@/features/playlists/list/paginated-playlists.tsx'\n\nexport function PlaylistsWithFiltersPage() {\n  return (\n    <div>\n      <PaginatedPlaylists />\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/api/client.ts",
    "content": "import createClient, { type Middleware } from 'openapi-fetch'\n\nimport type { paths } from './schema.ts'\n\nconst config = {\n  baseURL: null as string | null,\n  apiKey: null as string | null,\n  getAccessToken: null as (() => Promise<string | null>) | null,\n  saveAccessToken: null as ((accessToken: string | null) => Promise<void>) | null,\n  getRefreshToken: null as (() => Promise<string | null>) | null,\n  saveRefreshToken: null as ((refreshToken: string | null) => Promise<void>) | null,\n}\n\nexport const setClientConfig = (newConfig: Partial<typeof config>) => {\n  Object.assign(config, newConfig)\n  _client = undefined // пере-инициализируем\n}\n\nexport const getClientConfig = () => ({ ...config })\n\n/* ------------------------------------------------------------------ */\n/* 2.  Mutex для refresh-а                                             */\n/* ------------------------------------------------------------------ */\nlet refreshPromise: Promise<string> | null = null\n\nfunction makeRefreshToken(): Promise<string> {\n  if (!refreshPromise) {\n    // 1) создаём «замок» сразу\n    refreshPromise = (async (): Promise<string> => {\n      const refreshToken = await config.getRefreshToken!()\n      if (!refreshToken) throw new Error('No refresh token')\n\n      const res = await fetch(`${config.baseURL}/auth/refresh`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'API-KEY': config.apiKey!,\n        },\n        body: JSON.stringify({ refreshToken }),\n      })\n      if (res.status !== 201) throw new Error('Refresh failed')\n\n      const { accessToken, refreshToken: newRT } = await res.json()\n      await config.saveAccessToken!(accessToken)\n      await config.saveRefreshToken!(newRT)\n\n      return accessToken\n    })().finally(() => {\n      refreshPromise = null // 2) снимаем «замок»\n    })\n  }\n\n  return refreshPromise\n}\n\nconst authMiddleware: Middleware = {\n  /* ---------- REQUEST -------------------------------------------------- */\n  async onRequest({ request }) {\n    request.headers.set('API-KEY', config.apiKey!)\n\n    const token = await config.getAccessToken?.()\n    if (token) request.headers.set('Authorization', `Bearer ${token}`)\n    ;(request as any)._retryClone = request.clone()\n\n    return request\n  },\n  async onResponse({ request, response }) {\n    const req = request as Request & { _retry: boolean }\n\n    if (response.status !== 401 || request.url.includes('/auth/refresh')) {\n      return response // всё ок\n    }\n\n    // уже пытались? -> отдаём 401 наружу, чтобы не зациклиться\n    if (req._retry) return response\n    req._retry = true\n\n    try {\n      const newToken = await makeRefreshToken()\n\n      // повторяем исходный запрос с новым токеном\n      const orig = (req as any)._retryClone as Request // клон с целым body\n      const retry = new Request(orig, { headers: new Headers(orig.headers) })\n      retry.headers.set('Authorization', `Bearer ${newToken}`)\n      return await fetch(retry)\n    } catch (error) {\n      console.log(error)\n      await config.saveAccessToken!(null)\n      await config.saveRefreshToken!(null)\n      return response\n    }\n  },\n}\n\nlet _client: ReturnType<typeof createClient<paths>> | undefined\n\nexport const getClient = () => {\n  if (_client) return _client\n\n  if (!config.baseURL || !config.apiKey) {\n    console.error('call setClientConfig to setup api')\n    throw new Error('call setClientConfig to setup api')\n  }\n\n  const client = createClient<paths>({ baseUrl: config.baseURL })\n  client.use(authMiddleware)\n  _client = client\n  return _client\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/api/index.ts",
    "content": "export { getClient, setClientConfig } from './client'\nexport * from './schema'\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/api/json-api-error.ts",
    "content": "export interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport type ExtractError<T> = T extends { error?: infer E } ? E : unknown\n\n/* --- типы ошибок, совпадающие с фильтром -------------------------------- */\nexport interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport function isJsonApiErrorDocument(error: unknown): error is JsonApiErrorDocument {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    // @ts-expect-error type no matter\n    Array.isArray(error.errors)\n  )\n}\n\nexport function parseJsonApiErrors(errorDoc: JsonApiErrorDocument): {\n  fieldErrors: Record<string, string>\n  globalErrors: string[]\n} {\n  const fieldErrors: Record<string, string> = {}\n  const globalErrors: string[] = []\n\n  for (const err of errorDoc.errors) {\n    const msg = err.detail ?? err.title ?? 'Unknown error'\n    const ptr = err.source?.pointer\n    if (ptr) {\n      // убираем префикс JSON:API\n      const field = ptr.replace(/^\\/data\\/attributes\\//, '')\n      fieldErrors[field] = msg\n    } else {\n      globalErrors.push(msg)\n    }\n  }\n\n  return { fieldErrors, globalErrors }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/api/query-error-handler-for-rhf-factory.ts",
    "content": "import type { FieldValues, Path, UseFormSetError } from 'react-hook-form'\nimport { toast } from 'react-toastify'\n\nimport type { MutationMeta } from '../../app/routes/__root.tsx'\nimport {\n  isJsonApiErrorDocument,\n  type JsonApiErrorDocument,\n  parseJsonApiErrors,\n} from './json-api-error.ts'\n\nexport const queryErrorHandlerForRHFFactory = <T extends FieldValues>({\n  setError,\n}: {\n  setError?: UseFormSetError<T>\n}) => {\n  return (err: JsonApiErrorDocument) => {\n    // 400 от сервера в JSON:API формате\n    if (isJsonApiErrorDocument(err)) {\n      const { fieldErrors, globalErrors } = parseJsonApiErrors(err)\n\n      // полевые ошибки\n      for (const [field, message] of Object.entries(fieldErrors)) {\n        setError?.(field as Path<T>, { type: 'server', message })\n      }\n\n      // «глобальные» (без pointer)\n      if (globalErrors.length > 0) {\n        setError?.('root.server', {\n          type: 'server',\n          message: globalErrors.join('\\n'),\n        })\n        toast(globalErrors.join('\\n'))\n      }\n\n      return\n    }\n  }\n}\n\nexport const mutationGlobalErrorHandler = (\n  error: Error,\n  _: unknown,\n  __: unknown,\n  mutation: unknown\n) => {\n  // 400 от сервера в JSON:API формате\n  // @ts-expect-error ignore typing\n  const globalFlag = (mutation.meta as MutationMeta)?.globalErrorHandler\n  // если в meta сказали \"off\" — ничего не делаем\n  if (globalFlag === 'off') {\n    return\n  }\n\n  if (isJsonApiErrorDocument(error)) {\n    const { globalErrors } = parseJsonApiErrors(error)\n\n    // «глобальные» (без pointer)\n    if (globalErrors.length > 0) {\n      toast(globalErrors.join('\\n'))\n    }\n  }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/api/request-wrapper.ts",
    "content": "// types/api.ts\n\nimport { type ExtractError } from './json-api-error.ts'\n\n//-----------------------------------------------------------------------------\n// utils/requestWrapper.ts\n//-----------------------------------------------------------------------------\n// «Умный» обёртчик: Infers Data и Error из P,\n// возвращает Promise<Data>, а в случае ошибки — throw Error\nexport type ExtractData<T> = T extends { data?: infer D } ? NonNullable<D> : never\n\nexport async function requestWrapper<P extends Promise<{ data?: unknown; error?: unknown }>>(\n  promise: P\n): Promise<ExtractData<Awaited<P>>> {\n  const res = (await promise) as Awaited<P>\n  if ((res as { error?: unknown }).error) {\n    // здесь E = ExtractError<Awaited<P>>\n    throw (res as { error: ExtractError<Awaited<P>> }).error\n  }\n  return (res as { data: ExtractData<Awaited<P>> }).data\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/api/schema.ts",
    "content": "/**\n * This file was auto-generated by openapi-typescript.\n * Do not make direct changes to the file.\n */\n\nexport interface paths {\n  '/playlists/my': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Получить список моих плейлистов\n     * @deprecated\n     */\n    get: operations['PlaylistsController_getMyPlaylists']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить список всех плейлистов */\n    get: operations['PlaylistsPublicController_getPlaylists']\n    put?: never\n    /** Создать новый плейлист */\n    post: operations['PlaylistsController_createPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить один плейлист по ID */\n    get: operations['PlaylistsPublicController_getPlaylistById']\n    /** Обновить плейлист */\n    put: operations['PlaylistsController_updatePlaylist']\n    post?: never\n    /** Удалить плейлист */\n    delete: operations['PlaylistsController_deletePlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Переупорядочить плейлисты */\n    put: operations['PlaylistsController_reorderPlaylist']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/images/main': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /**\n     * Загрузить обложку для плейлиста\n     * @description Минимальная высота — 500px, квадратное изображение\n     */\n    post: operations['PlaylistsController_uploadMainImage']\n    /** Удалить обложку плейлиста */\n    delete: operations['PlaylistsController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить список всех треков во всех плейлистах */\n    get: operations['TracksPublicController_getAllTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить список треков внутри плейлиста */\n    get: operations['TracksPublicController_getPlaylistTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить детали трека по ID */\n    get: operations['TracksPublicController_getTrackDetails']\n    /** Обновить информацию о треке */\n    put: operations['TracksController_updateTrack']\n    post?: never\n    /** Полностью удалить трек */\n    delete: operations['TracksController_deleteTrackCompletely']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Поставить лайк треку или снять его (toggle) */\n    post: operations['TracksPublicController_likeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Поставить дизлайк треку или снять его (toggle) */\n    post: operations['TracksPublicController_dislikeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить реакцию пользователя на трек */\n    delete: operations['TracksPublicController_removeTrackReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Поставить лайк плейлисту */\n    post: operations['PlaylistsPublicController_likePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Поставить дизлайк плейлисту */\n    post: operations['PlaylistsPublicController_dislikePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить реакцию пользователя на плейлист */\n    delete: operations['PlaylistsPublicController_removePlaylistReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks/{trackId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Изменить порядок треков в плейлисте */\n    put: operations['TracksController_reorderTrack']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Добавить трек в свой плейлист */\n    post: operations['TracksController_addTrackToPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить трек из своего плейлиста */\n    delete: operations['TracksController_unbindTrackFromPlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/actions/publish': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Публикация трека (сделать доступным для всех) */\n    post: operations['TracksController_publishTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/cover': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Загрузить обложку трека */\n    post: operations['TracksController_uploadTrackCover']\n    /** Удалить обложку трека */\n    delete: operations['TracksController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/upload': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Создать трек с загрузкой mp3 файла */\n    post: operations['TracksController_uploadTrackMp3']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Создать нового исполнителя */\n    post: operations['ArtistsController_createArtist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Поиск исполнителей по подстроке */\n    get: operations['ArtistsController_searchArtist']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить исполнителя по ID */\n    delete: operations['ArtistsController_deleteArtist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/oauth-redirect': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * OAuth редирект\n     * @description The callback URL to redirect after grand access,\n     *          <a target=\"_blank\" href=\"https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\">https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid</a>\n     */\n    get: operations['AuthController_OauthRedirect']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/login': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Залогиниться с помощью кода, полученного после редиректа после авторизации через OAuth */\n    post: operations['AuthController_login']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/refresh': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Обновить пару refresh/access токенов */\n    post: operations['AuthController_refresh']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/logout': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Деактивировать refresh-token */\n    post: operations['AuthController_logout']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/me': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить текущего пользователя по access токену */\n    get: operations['AuthController_getMe']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Создать новый тег */\n    post: operations['TagsController_createTag']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Поиск тегов по подстроке */\n    get: operations['TagsController_searchTags']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Удалить тег по ID */\n    delete: operations['TagsController_deleteTag']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n}\nexport type webhooks = Record<string, never>\nexport interface components {\n  schemas: {\n    UserOutputDTO: {\n      id: string\n      name: string\n    }\n    ImageDto: {\n      /** @enum {string} */\n      type: 'original' | 'thumbnail' | 'medium'\n      width: number\n      height: number\n      fileSize: number\n      url: string\n    }\n    PlaylistImagesOutputDTO: {\n      /** @description Оригинальное изображение и превьюшки */\n      main?: components['schemas']['ImageDto'][]\n    }\n    /**\n     * @description 0 (не залогинен или не реагировал), 1 — лайк, -1 — дизлайк\n     * @enum {number}\n     */\n    ReactionValue: 0 | 1 | -1\n    PlaylistAttributesDto: {\n      title: string\n      description: string | null\n      addedAt: string\n      updatedAt: string\n      order: number\n      user: components['schemas']['UserOutputDTO']\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      tags: string[]\n      likesCount: number\n      dislikesCount: number\n      /** @description 0 (не залогинен или не реагировал), 1 — лайк, -1 — дизлайк */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistListItemJsonApiData: {\n      id: string\n      /** @example playlists */\n      type: string\n      attributes: components['schemas']['PlaylistAttributesDto']\n    }\n    GetMyPlaylistsOutput: {\n      data: components['schemas']['PlaylistListItemJsonApiData'][]\n    }\n    CreatePlaylistRequestPayload: {\n      title: string\n      description?: string\n    }\n    PlaylistOutputAttributes: {\n      title: string\n      description: string | null\n      addedAt: string\n      updatedAt: string\n      order: number\n      user: components['schemas']['UserOutputDTO']\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      tags: string[]\n      likesCount: number\n      dislikesCount: number\n      /** @description 0 (не залогинен или не реагировал), 1 — лайк, -1 — дизлайк */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistOutput: {\n      id: string\n      /** @example playlists */\n      type: string\n      attributes: components['schemas']['PlaylistOutputAttributes']\n    }\n    GetPlaylistOutput: {\n      data: components['schemas']['PlaylistOutput']\n    }\n    UpdatePlaylistRequestPayload: {\n      title: string\n      /** @example Cool playlist */\n      description?: string | null\n      tagIds?: string[]\n    }\n    ReorderPlaylistsRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID плейлиста, после которого нужно вставить текущий. null - разместить плейлист в начало списка.\n       * @example a1b2c3d4-e5f6-7890-abcd-1234567890ef\n       */\n      putAfterItemId?: string | null\n    }\n    GetImagesOutput: {\n      /** @description Должен содержать оригинальный размер изображения и миниатюры, например: original, 320x180 и т.п. */\n      main?: components['schemas']['ImageDto'][]\n    }\n    GetTracksRequestPayload: {\n      /**\n       * @description Номер страницы для пагинации (начиная с 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Размер страницы для пагинации (от 1 до 20)\n       * @default 10\n       */\n      pageSize: number\n      /** @description Строка для поиска по названию плейлиста */\n      search?: string\n      /**\n       * @description Поле, по которому сортируются треки\n       * @default publishedAt\n       * @enum {string}\n       */\n      sortBy: 'publishedAt' | 'likesCount'\n      /**\n       * @description Направление сортировки (по возрастанию или убыванию)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: 'asc' | 'desc'\n      /** @description Фильтрация по ID тегов (можно передавать несколько) */\n      tagsIds?: string[]\n      /** @description Фильтрация по ID артистов (можно передавать несколько) */\n      artistsIds?: string[]\n      /** @description Фильтрация по ID пользователя (создателя трека) */\n      userId?: string\n      /** @description Если true — включать в выдачу также ваши неопубликованные треки (drafts) */\n      includeOwnUnpublished?: boolean\n      /**\n       * @description Тип пагинации: `offset` — по номеру страницы; `cursor` — keyset/seek.\n       * @default offset\n       * @enum {string}\n       */\n      paginationType: 'offset' | 'cursor'\n      /** @description Base64-закодированный курсор для keyset-пагинации. Используется только если paginationType=cursor. */\n      cursor?: string\n    }\n    AttachmentDto: {\n      id: string\n      /** Format: date-time */\n      addedAt: string\n      /** Format: date-time */\n      updatedAt: string\n      version: number\n      /**\n       * @description Public URL to access the uploaded file\n       * @example https://cdn.example.com/uploads/track123/cover.jpg\n       */\n      url: string\n      /**\n       * @description MIME type of the file\n       * @example image/jpeg\n       */\n      contentType: string\n      /**\n       * @description Original filename uploaded by the user\n       * @example cover.jpg\n       */\n      originalName: string\n      /**\n       * @description Size of the file in bytes\n       * @example 34872\n       */\n      fileSize: number\n    }\n    TrackListItemOutputAttributes: {\n      title: string\n      addedAt: string\n      attachments: components['schemas']['AttachmentDto'][]\n      images: components['schemas']['GetImagesOutput']\n      user: components['schemas']['UserOutputDTO']\n      /**\n       * @description 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n      isPublished: boolean\n      publishedAt?: string\n    }\n    ArtistRelationship: {\n      id: string\n      type: string\n    }\n    ArtistsRelationship: {\n      data: components['schemas']['ArtistRelationship'][]\n    }\n    TrackRelationships: {\n      artists: components['schemas']['ArtistsRelationship']\n    }\n    TrackListItemOutput: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackListItemOutputAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMetaWithPagingAndCursor: {\n      page: number\n      pageSize: number\n      totalCount: number | null\n      pagesCount: number | null\n      nextCursor: string | null\n    }\n    OmitTypeClass: {\n      name: string\n    }\n    IncludedArtistOutput: {\n      id: string\n      type: string\n      attributes: components['schemas']['OmitTypeClass']\n    }\n    GetTrackListOutput: {\n      data: components['schemas']['TrackListItemOutput'][]\n      meta: components['schemas']['JsonApiMetaWithPagingAndCursor']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    PlaylistTrackAttributes: {\n      title: string\n      order: number\n      addedAt: string\n      updatedAt: string\n      attachments: unknown[][]\n      images: components['schemas']['GetImagesOutput']\n      /**\n       * @description 0 (не залогинен или не реагировал), 1 — лайк, -1 — дизлайк\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n    }\n    GetPlaylistTrackListOutputData: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['PlaylistTrackAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMeta: {\n      totalCount: number\n    }\n    GetPlaylistTrackListOutput: {\n      data: components['schemas']['GetPlaylistTrackListOutputData'][]\n      meta: components['schemas']['JsonApiMeta']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    GetTagOutput: {\n      id: string\n      name: string\n    }\n    GetArtistOutput: {\n      id: string\n      name: string\n    }\n    TrackDetailsAttributes: {\n      title: string\n      lyrics?: string\n      releaseDate?: string\n      addedAt: string\n      /** Format: iso8601 */\n      updatedAt: string\n      duration: number\n      likesCount: number\n      dislikesCount: number\n      attachments: components['schemas']['AttachmentDto'][]\n      images: components['schemas']['GetImagesOutput']\n      tags: components['schemas']['GetTagOutput'][]\n      artists: components['schemas']['GetArtistOutput'][]\n      isPublished: boolean\n      publishedAt?: string\n      /**\n       * @description 0 – гость или не реагировал, 1 – пользователь лайкнул, -1 – пользователь дизлайкнул\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n    }\n    TrackDetailsData: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackDetailsAttributes']\n    }\n    GetTrackDetailsOutput: {\n      data: components['schemas']['TrackDetailsData']\n    }\n    ReactionOutput: {\n      objectId: string\n      /** @enum {number} */\n      value: 0 | 1 | -1\n      likes: number\n      dislikes: number\n    }\n    GetPlaylistsRequestPayload: {\n      /**\n       * @description Номер страницы для пагинации (начиная с 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Размер страницы для пагинации (от 1 до 20)\n       * @default 10\n       */\n      pageSize: number\n      /** @description Строка для поиска по названию плейлиста */\n      search?: string\n      /**\n       * @description Поле, по которому выполняется сортировка\n       * @default publishedAt\n       * @enum {string}\n       */\n      sortBy: 'publishedAt' | 'likesCount'\n      /**\n       * @description Направление сортировки (по возрастанию или убыванию)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: 'asc' | 'desc'\n      /** @description Фильтрация по ID тегов. Может быть передано несколько значений: tagsIds=tag1&tagsIds=tag2 */\n      tagsIds?: string[]\n      /** @description Фильтрация по ID пользователя (создателя плейлиста) */\n      userId?: string\n      /** @description Фильтрация по ID трека — только те плейлисты, в которых он содержится */\n      trackId?: string\n    }\n    JsonApiMetaWithPaging: {\n      totalCount: number\n      page: number\n      pageSize: number\n      pagesCount: number\n    }\n    GetPlaylistsOutput: {\n      data: components['schemas']['PlaylistListItemJsonApiData'][]\n      meta: components['schemas']['JsonApiMetaWithPaging']\n    }\n    ReorderTracksRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID трека, после которого нужно вставить текущий. null - разместить трек в начало списка.\n       * @example a1b2c3d4-e5f6-7890-abcd-1234567890ef\n       */\n      putAfterItemId?: string | null\n    }\n    UpdateTrackRequestPayload: {\n      title: string\n      /** @description Текст песни (lyrics) */\n      lyrics?: string\n      /** Format: iso8601 */\n      releaseDate?: string\n      tagIds?: string[]\n      artistsIds?: string[]\n    }\n    TrackOutputAttributes: {\n      title: string\n      lyrics?: string\n      releaseDate?: string\n      addedAt: string\n      /** Format: iso8601 */\n      updatedAt: string\n      duration: number\n      likesCount: number\n      dislikesCount: number\n      attachments: components['schemas']['AttachmentDto'][]\n      images: components['schemas']['GetImagesOutput']\n      tags: components['schemas']['GetTagOutput'][]\n      artists: components['schemas']['GetArtistOutput'][]\n      isPublished: boolean\n      publishedAt?: string\n      /**\n       * @description 0 – гость или не реагировал, 1 – пользователь лайкнул, -1 – пользователь дизлайкнул\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n    }\n    TrackOutput: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackOutputAttributes']\n    }\n    GetTrackOutput: {\n      data: components['schemas']['TrackOutput']\n    }\n    AddTrackToPlaylistRequestPayload: {\n      /** @description ID of the track to add to the playlist */\n      trackId: string\n    }\n    CreateArtistRequestPayload: {\n      name: string\n    }\n    LoginRequestPayload: {\n      /** @description Код, полученный от oauth-сервер после редиректа */\n      code: string\n      /**\n       * @description Укажите тоже значение, что и во время первого запроса на oauth-сервер\n       * @example http://localhost:3000/oauth2/callback\n       */\n      redirectUri: string\n      /**\n       * @description Срок жизни accessToken-а (по дефолту \"3m\"), Можно использовать значение в формате: be a string like \"60s\", \"3m\", \"2h\", \"1d\"\n       * @example 3m\n       */\n      accessTokenTTL?: string\n      /** @description Как долго будет жить refreshToken. Если true - 1 месяц, если false - 30 минут. Явно указанный accessTokenTTL не должен быть больше, чем время жизни refreshToken */\n      rememberMe: boolean\n    }\n    RefreshOutput: {\n      refreshToken: string\n      accessToken: string\n    }\n    BadRequestException: Record<string, never>\n    UnauthorizedException: Record<string, never>\n    RefreshRequestPayload: {\n      refreshToken: string\n    }\n    LogoutRequestPayload: {\n      refreshToken: string\n    }\n    GetMeOutput: {\n      userId: string\n      login: string\n    }\n    CreateTagRequestPayload: {\n      name: string\n    }\n    /**\n     * Format: binary\n     * @description Файл в multipart/form-data\n     */\n    BinaryFile: string\n  }\n  responses: never\n  parameters: never\n  requestBodies: never\n  headers: never\n  pathItems: never\n}\nexport type SchemaUserOutputDto = components['schemas']['UserOutputDTO']\nexport type SchemaImageDto = components['schemas']['ImageDto']\nexport type SchemaPlaylistImagesOutputDto = components['schemas']['PlaylistImagesOutputDTO']\nexport type SchemaReactionValue = components['schemas']['ReactionValue']\nexport type SchemaPlaylistAttributesDto = components['schemas']['PlaylistAttributesDto']\nexport type SchemaPlaylistListItemJsonApiData = components['schemas']['PlaylistListItemJsonApiData']\nexport type SchemaGetMyPlaylistsOutput = components['schemas']['GetMyPlaylistsOutput']\nexport type SchemaCreatePlaylistRequestPayload =\n  components['schemas']['CreatePlaylistRequestPayload']\nexport type SchemaPlaylistOutputAttributes = components['schemas']['PlaylistOutputAttributes']\nexport type SchemaPlaylistOutput = components['schemas']['PlaylistOutput']\nexport type SchemaGetPlaylistOutput = components['schemas']['GetPlaylistOutput']\nexport type SchemaUpdatePlaylistRequestPayload =\n  components['schemas']['UpdatePlaylistRequestPayload']\nexport type SchemaReorderPlaylistsRequestPayload =\n  components['schemas']['ReorderPlaylistsRequestPayload']\nexport type SchemaGetImagesOutput = components['schemas']['GetImagesOutput']\nexport type SchemaGetTracksRequestPayload = components['schemas']['GetTracksRequestPayload']\nexport type SchemaAttachmentDto = components['schemas']['AttachmentDto']\nexport type SchemaTrackListItemOutputAttributes =\n  components['schemas']['TrackListItemOutputAttributes']\nexport type SchemaArtistRelationship = components['schemas']['ArtistRelationship']\nexport type SchemaArtistsRelationship = components['schemas']['ArtistsRelationship']\nexport type SchemaTrackRelationships = components['schemas']['TrackRelationships']\nexport type SchemaTrackListItemOutput = components['schemas']['TrackListItemOutput']\nexport type SchemaJsonApiMetaWithPagingAndCursor =\n  components['schemas']['JsonApiMetaWithPagingAndCursor']\nexport type SchemaOmitTypeClass = components['schemas']['OmitTypeClass']\nexport type SchemaIncludedArtistOutput = components['schemas']['IncludedArtistOutput']\nexport type SchemaGetTrackListOutput = components['schemas']['GetTrackListOutput']\nexport type SchemaPlaylistTrackAttributes = components['schemas']['PlaylistTrackAttributes']\nexport type SchemaGetPlaylistTrackListOutputData =\n  components['schemas']['GetPlaylistTrackListOutputData']\nexport type SchemaJsonApiMeta = components['schemas']['JsonApiMeta']\nexport type SchemaGetPlaylistTrackListOutput = components['schemas']['GetPlaylistTrackListOutput']\nexport type SchemaGetTagOutput = components['schemas']['GetTagOutput']\nexport type SchemaGetArtistOutput = components['schemas']['GetArtistOutput']\nexport type SchemaTrackDetailsAttributes = components['schemas']['TrackDetailsAttributes']\nexport type SchemaTrackDetailsData = components['schemas']['TrackDetailsData']\nexport type SchemaGetTrackDetailsOutput = components['schemas']['GetTrackDetailsOutput']\nexport type SchemaReactionOutput = components['schemas']['ReactionOutput']\nexport type SchemaGetPlaylistsRequestPayload = components['schemas']['GetPlaylistsRequestPayload']\nexport type SchemaJsonApiMetaWithPaging = components['schemas']['JsonApiMetaWithPaging']\nexport type SchemaGetPlaylistsOutput = components['schemas']['GetPlaylistsOutput']\nexport type SchemaReorderTracksRequestPayload = components['schemas']['ReorderTracksRequestPayload']\nexport type SchemaUpdateTrackRequestPayload = components['schemas']['UpdateTrackRequestPayload']\nexport type SchemaTrackOutputAttributes = components['schemas']['TrackOutputAttributes']\nexport type SchemaTrackOutput = components['schemas']['TrackOutput']\nexport type SchemaGetTrackOutput = components['schemas']['GetTrackOutput']\nexport type SchemaAddTrackToPlaylistRequestPayload =\n  components['schemas']['AddTrackToPlaylistRequestPayload']\nexport type SchemaCreateArtistRequestPayload = components['schemas']['CreateArtistRequestPayload']\nexport type SchemaLoginRequestPayload = components['schemas']['LoginRequestPayload']\nexport type SchemaRefreshOutput = components['schemas']['RefreshOutput']\nexport type SchemaBadRequestException = components['schemas']['BadRequestException']\nexport type SchemaUnauthorizedException = components['schemas']['UnauthorizedException']\nexport type SchemaRefreshRequestPayload = components['schemas']['RefreshRequestPayload']\nexport type SchemaLogoutRequestPayload = components['schemas']['LogoutRequestPayload']\nexport type SchemaGetMeOutput = components['schemas']['GetMeOutput']\nexport type SchemaCreateTagRequestPayload = components['schemas']['CreateTagRequestPayload']\nexport type SchemaBinaryFile = components['schemas']['BinaryFile']\nexport type $defs = Record<string, never>\nexport interface operations {\n  PlaylistsController_getMyPlaylists: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Список плейлистов успешно получен */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMyPlaylistsOutput']\n        }\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylists: {\n    parameters: {\n      query?: {\n        /** @description Номер страницы для пагинации (начиная с 1) */\n        pageNumber?: number\n        /** @description Размер страницы для пагинации (от 1 до 20) */\n        pageSize?: number\n        /** @description Строка для поиска по названию плейлиста */\n        search?: string\n        /** @description Поле, по которому выполняется сортировка */\n        sortBy?: 'publishedAt' | 'likesCount'\n        /** @description Направление сортировки (по возрастанию или убыванию) */\n        sortDirection?: 'asc' | 'desc'\n        /** @description Фильтрация по ID тегов. Может быть передано несколько значений: tagsIds=tag1&tagsIds=tag2 */\n        tagsIds?: string[]\n        /** @description Фильтрация по ID пользователя (создателя плейлиста) */\n        userId?: string\n        /** @description Фильтрация по ID трека — только те плейлисты, в которых он содержится */\n        trackId?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: JSON:API список плейлистов с пагинацией */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistsOutput']\n        }\n      }\n    }\n  }\n  PlaylistsController_createPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Плейлист успешно создан */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Forbidden: Превышен лимит создания плейлистов */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylistById: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID плейлиста */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Плейлист успешно найден */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description NotFound: Плейлист с таким ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_updatePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description NoContent: Плейлист успешно обновлён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description BadRequest: Ошибка валидации, например, превышено количество тегов (более 5) */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Пользователь не имеет прав для обновления данного плейлиста */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deletePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Плейлист успешно удалён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Недостаточно прав для удаления плейлиста */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_reorderPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderPlaylistsRequestPayload']\n      }\n    }\n    responses: {\n      /** @description NoContent: Порядок плейлистов успешно изменён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Playlist not found или putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_uploadMainImage: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @description Максимальный размер 1 MB, Минимальная высота — 500px, квадратное изображение */\n          file: components['schemas']['BinaryFile']\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Обложка успешно загружена */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetImagesOutput']\n        }\n      }\n      /** @description BadRequest: Ошибка формата или размеров изображения */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нет прав на загрузку изображения в плейлист */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Обложка удалена */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Удаление обложки плейлиста другого пользователя запрещена */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getAllTracks: {\n    parameters: {\n      query?: {\n        /** @description Номер страницы для пагинации (начиная с 1) */\n        pageNumber?: number\n        /** @description Размер страницы для пагинации (от 1 до 20) */\n        pageSize?: number\n        /** @description Строка для поиска по названию плейлиста */\n        search?: string\n        /** @description Поле, по которому сортируются треки */\n        sortBy?: 'publishedAt' | 'likesCount'\n        /** @description Направление сортировки (по возрастанию или убыванию) */\n        sortDirection?: 'asc' | 'desc'\n        /** @description Фильтрация по ID тегов (можно передавать несколько) */\n        tagsIds?: string[]\n        /** @description Фильтрация по ID артистов (можно передавать несколько) */\n        artistsIds?: string[]\n        /** @description Фильтрация по ID пользователя (создателя трека) */\n        userId?: string\n        /** @description Если true — включать в выдачу также ваши неопубликованные треки (drafts) */\n        includeOwnUnpublished?: boolean\n        /** @description Тип пагинации: `offset` — по номеру страницы; `cursor` — keyset/seek. */\n        paginationType?: 'offset' | 'cursor'\n        /** @description Base64-закодированный курсор для keyset-пагинации. Используется только если paginationType=cursor. */\n        cursor?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Пагинированный список треков */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackListOutput']\n        }\n      }\n    }\n  }\n  TracksPublicController_getPlaylistTracks: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID плейлиста, для которого необходимо получить треки */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Список треков в плейлисте */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistTrackListOutput']\n        }\n      }\n      /** @description NotFound: Плейлист с указанным ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getTrackDetails: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID трека, для которого необходимо получить детали */\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Детали трека с вложениями */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description NotFound: Трек с таким ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_updateTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdateTrackRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Трек успешно обновлён */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackOutput']\n        }\n      }\n      /** @description BadRequest: Превышено количество тегов или артистов */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нельзя редактировать чужой трек */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек или плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCompletely: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Трек полностью удалён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Удаление чужого трека запрещено */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_likeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Текущая реакция пользователя + суммарные счётчики */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description BadRequest: Некорректный идентификатор */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_dislikeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description BadRequest: Некорректный ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_removeTrackReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_likePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description BadRequest: Некорректный ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_dislikePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description BadRequest: Некорректный ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_removePlaylistReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_reorderTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderTracksRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Порядок трека обновлён */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description BadRequest: Нельзя поставить после самого себя */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нет доступа к плейлисту */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек или putAfterItemId не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_addTrackToPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['AddTrackToPlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description NoContent: Трек успешно добавлен в плейлист */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нет доступа к плейлисту или превышен лимит треков в плейлисте: максимум 10 треков */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_unbindTrackFromPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Трек удалён из плейлиста */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нет доступа к плейлисту */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Плейлист не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_publishTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Трек успешно опубликован */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нельзя публиковать чужие треки */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Трек с указанным ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Трек уже опубликован */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID трека, которому загружается обложка */\n        trackId: string\n      }\n      cookie?: never\n    }\n    /** @description Файл изображения.<br/>\n     *                       • Имя поля — <code>cover</code><br/>\n     *                       • Допустимые MIME-типы — <code>image/jpeg</code>, <code>image/png</code>, <code>image/gif</code><br/>\n     *                       • Максимальный размер — <code>100 KB</code> */\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** Format: binary */\n          cover: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Успешная загрузка обложки */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetImagesOutput']\n        }\n      }\n      /** @description BadRequest: Неверный файл или превышен размер */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Нельзя загружать обложку к чужому треку */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Обложка удалена */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Удаление обложки трека другого пользователя запрещена */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Трек не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackMp3: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @example My cool track */\n          title: string\n          /** Format: binary */\n          file: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Трек успешно создан */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackOutput']\n        }\n      }\n      /** @description BadRequest: Неверный формат файла или превышен размер */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description InternalServerError: Ошибка при сохранении файла или трека */\n      500: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_createArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateArtistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Исполнитель успешно создан */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetArtistOutput']\n        }\n      }\n      /** @description BadRequest: Ошибка валидации или неверный ввод */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Лимит в 100 артистов на пользователя исчерпан */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Исполнитель с таким именем уже существует */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_searchArtist: {\n    parameters: {\n      query: {\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Список исполнителей найден по подстроке */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetArtistOutput'][]\n        }\n      }\n    }\n  }\n  ArtistsController_deleteArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Исполнитель успешно удалён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Другой юзер создал данного артиста или данный артист прикреплён к трекам */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Исполнитель с таким ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_OauthRedirect: {\n    parameters: {\n      query?: {\n        /** @description The callback URL to redirect after grand access,\n         *          https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid */\n        callbackUrl?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Редирект выполнен */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_login: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LoginRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Успешно получена пара токенов */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description BadRequest: Неверный формат запроса или отсутствуют обязательные параметры */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['BadRequestException']\n        }\n      }\n      /** @description Unauthorized: Код недействителен, истёк или не передан, или не совпадает redirectUri */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_refresh: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['RefreshRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Успешное обновление пары токенов */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Unauthorized: Refresh-token недействителен, истёк или не передан */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_logout: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LogoutRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: refresh токен деактивирован, при этом access-токен остаётся ещё валидным. */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_getMe: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Успешное получение информации о пользователе */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMeOutput']\n        }\n      }\n      /** @description Unauthorized: access токен отсутствует или недействителен */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_createTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateTagRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Тег успешно создан */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput']\n        }\n      }\n      /** @description BadRequest: Ошибка валидации */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Лимит в 100 тегов на пользователя исчерпан */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Тег с таким именем уже существует */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_searchTags: {\n    parameters: {\n      query: {\n        /** @description Подстрока для поиска тегов (по нормализованному имени) */\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Список подходящих тегов */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput'][]\n        }\n      }\n      /** @description BadRequest: Некорректный поисковый запрос */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_deleteTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID удаляемого тега */\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description NoContent: Тег успешно удалён */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: Пользователь не авторизован */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Другой юзер создал данный тег или данй тег прикреплён к трекам и плейлистам */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description NotFound: Тег с указанным ID не найден */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/api/socket.ts",
    "content": "import { io, Socket } from 'socket.io-client'\n\nlet sharedSocket: Socket | null = null\n\nconst BASE = import.meta.env.VITE_BASE_URL.replace('api/1.0', '')\nconst PATH = '/api/1.0/ws'\n\n/** создать новый socket с (или без) токена */\nfunction createSocket(token: string | null): Socket {\n  return io(BASE, {\n    path: PATH,\n    transports: ['websocket'],\n    ...(token ? { auth: { token } } : {}),\n  })\n}\n\n/** получить singleton-сокет (гость, если токена нет) */\nexport function getSharedSocket(token: string | null): Socket {\n  if (!sharedSocket) sharedSocket = createSocket(token)\n  return sharedSocket\n}\n\n/**\n * вызвать после логина / логаута.\n * – token === null  → переключаемся на гостевой сокет\n * – token === 'XXX' → подключаемся авторизованно\n */\nexport function resetSocketWithToken(token: string | null): Socket {\n  // аккуратно рвём старое соединение\n  if (sharedSocket) {\n    sharedSocket.disconnect()\n  }\n\n  sharedSocket = createSocket(token)\n  return sharedSocket\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/config/api.config.ts",
    "content": "export const apiBaseUrl = import.meta.env.VITE_BASE_URL\nexport const apiKey = import.meta.env.VITE_API_KEY\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/db/localstorage-keys.ts",
    "content": "export const localStorageKeys = {\n  refreshToken: 'musicfun-refresh-token',\n  accessToken: 'musicfun-access-token',\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/routes/routes.ts",
    "content": "export const ROUTES = {\n  main: '/',\n  myPlaylists: '/my-playlists',\n} as const\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/ui/header/header.component.tsx",
    "content": "import { Link } from '@tanstack/react-router'\nimport type { ReactNode } from 'react'\n\nimport styles from './header.module.css'\n\ntype Props = {\n  renderAccountBar: () => ReactNode\n}\n\nexport const Header = ({ renderAccountBar }: Props) => (\n  <header className={styles.header}>\n    <div className={styles.container}>\n      <div className={styles.linksBlock}>\n        <Link to=\"/\">Main</Link>\n        <Link to=\"/playlists-with-filters\">Playlists</Link>\n      </div>\n\n      <div>{renderAccountBar()}</div>\n    </div>\n  </header>\n)\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/ui/header/header.module.css",
    "content": ".header {\n  border-bottom: #aaaaaa 1px solid;\n  padding-bottom: 10px;\n}\n\n.container {\n  max-width: 900px;\n  margin: 0 auto;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n}\n\n.linksBlock {\n  display: flex;\n  gap: 10px;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/ui/pagination/pagination-nav/pagination-nav.module.css",
    "content": ".pagination {\n  display: flex;\n  gap: 8px;\n  justify-content: center;\n}\n\n.pageButton {\n  padding: 4px 10px;\n  background: transparent;\n  border: 1px solid #aaa;\n  border-radius: 4px;\n  cursor: pointer;\n  font-weight: normal;\n  transition:\n    background 0.2s,\n    color 0.2s;\n  color: white;\n}\n\n.pageButtonActive {\n  background: #ececec;\n  font-weight: bold;\n  cursor: default;\n  color: black;\n}\n\n.ellipsis {\n  padding: 4px 10px;\n  user-select: none;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/ui/pagination/pagination-nav/pagination-nav.tsx",
    "content": "import { getPaginationPages } from '../utils/get-pagination-pages.ts'\nimport s from './pagination-nav.module.css'\n\ntype Props = {\n  current: number\n  pagesCount: number\n  onChange: (page: number) => void\n}\n\nconst SIBLING_COUNT = 1\n\nexport const PaginationNav = ({ current, pagesCount, onChange }: Props) => {\n  const pages = getPaginationPages(current, pagesCount, SIBLING_COUNT)\n\n  return (\n    <div className={s.pagination}>\n      {pages.map((item, idx) =>\n        item === '...' ? (\n          <span className={s.ellipsis} key={`ellipsis-${idx}`}>\n            ...\n          </span>\n        ) : (\n          <button\n            key={item}\n            className={item === current ? `${s.pageButton} ${s.pageButtonActive}` : s.pageButton}\n            onClick={() => item !== current && onChange(Number(item))}\n            disabled={item === current}\n            type=\"button\">\n            {item}\n          </button>\n        )\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/ui/pagination/pagination.module.css",
    "content": ".container {\n  display: flex;\n  align-content: center;\n  align-items: center;\n  margin: 0 auto;\n  gap: 40px;\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/ui/pagination/pagination.tsx",
    "content": "import s from './Pagination.module.css'\nimport { PaginationNav } from './pagination-nav/pagination-nav.tsx'\n\ntype Props = {\n  current: number\n  pagesCount: number\n  changePageNumber: (page: number) => void\n  isFetching: boolean\n}\n\nexport const Pagination = ({ current, pagesCount, changePageNumber, isFetching }: Props) => {\n  return (\n    <div className={s.container}>\n      <PaginationNav current={current} pagesCount={pagesCount} onChange={changePageNumber} />{' '}\n      {isFetching && '⌛️'}\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/shared/ui/pagination/utils/get-pagination-pages.ts",
    "content": "/**\n * Генерирует массив страниц для отображения пагинации с учётом троеточий\n */\nexport const getPaginationPages = (\n  current: number,\n  pagesCount: number,\n  siblingCount: number\n): (number | '...')[] => {\n  if (pagesCount <= 1) return []\n\n  const pages: (number | '...')[] = []\n\n  // Границы диапазона вокруг текущей страницы\n  const leftSibling = Math.max(2, current - siblingCount)\n  const rightSibling = Math.min(pagesCount - 1, current + siblingCount)\n\n  // Всегда показываем первую страницу\n  pages.push(1)\n\n  // Троеточие слева\n  if (leftSibling > 2) {\n    pages.push('...')\n  }\n\n  // Соседние страницы вокруг текущей\n  for (let page = leftSibling; page <= rightSibling; page++) {\n    pages.push(page)\n  }\n\n  // Троеточие справа\n  if (rightSibling < pagesCount - 1) {\n    pages.push('...')\n  }\n\n  // Всегда показываем последнюю страницу (если больше одной)\n  if (pagesCount > 1) {\n    pages.push(pagesCount)\n  }\n\n  return pages\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true,\n    \"baseUrl\": \".\", // allow non-relative imports\n    \"paths\": {\n      \"@/*\": [\"src/*\"] // map “@/foo” → “src/foo”\n    }\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\", // or \"NodeNext\"\n    \"moduleResolution\": \"Bundler\", // or \"NodeNext\"\n    \"noUncheckedIndexedAccess\": true\n  },\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/tsr.config.json",
    "content": "{\n  \"routesDirectory\": \"./src/app/routes\",\n  \"generatedRouteTree\": \"./src/app/routes/routeTree.gen.ts\"\n}\n"
  },
  {
    "path": "experiment-apps/musicfun-tanstack-query-small-example/vite.config.ts",
    "content": "import tanstackRouter from '@tanstack/router-plugin/vite'\nimport react from '@vitejs/plugin-react'\nimport path from 'path'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [\n    tanstackRouter({\n      target: 'react',\n      autoCodeSplitting: true,\n    }),\n    react(),\n  ],\n  resolve: {\n    alias: {\n      // “@” will map to the /src directory\n      '@': path.resolve(__dirname, 'src'),\n    },\n  },\n  server: {\n    host: true, // ← or '0.0.0.0'\n    port: 5174,\n    strictPort: true,\n    allowedHosts: [\n      'domain.prod', // <-- your custom host\n      'localhost', // (optional) keep localhost too\n    ],\n  },\n})\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# env files\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/.prettierrc",
    "content": "{\n  \"printWidth\": 120,\n  \"semi\": false\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/README.md",
    "content": "# Информация\n\n## 🔗 Ссылки\n\n### 📗 Swagger\n\n- [trelly](https://trelly.it-incubator.app/api)\n\n## 🚀 Запуск проекта\n\n### 1. Установка зависимостей\n\n```bash\npnpm i\n```\n\n### 2. Старт проекта\n\n```bash\n   pnpm dev\n```\n\n## 🔗 Настройки\n\n### 🕎 Переменные окружения\n\nВ файле `.env` замените `VITE_API_KEY` который нужно взять из [Api hub](https://apihub.it-incubator.io/en)\n\n### 🅿️ Prettier\n\n❗Обязательно включите в настройках Webstorm `prettier`\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>IT-INCUBATOR Todolist | Vite + React + TS</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/openapi-config.js",
    "content": "export default {\n  schemaFile: \"https://trelly.it-incubator.app/api-json\", // ❌ не генерируется\n  apiFile: \"./src/app/baseApi.ts\",\n  apiImport: \"baseApi\",\n  exportName: \"generatedApi\",\n  hooks: true,\n  tag: true,\n  argSuffix: \"Args\", // ArtistsControllerCreateArtistArgs\n  responseSuffix: \"Response\", // ArtistsControllerCreateArtistResponse\n  // Импорт сущностей в один файл\n  outputFile: \"./src/generated/generatedApi.ts\",\n  // Импорт сущностей в разные файлы\n  outputFiles: {\n    \"./src/generated/boardsController.ts\": {\n      filterEndpoints: [/boardsController/],\n    },\n    \"./src/generated/boardsPublicController.ts\": {\n      filterEndpoints: [/boardsPublicController/],\n    },\n    \"./src/generated/tasksPublicController.ts\": {\n      filterEndpoints: [/tasksPublicController/],\n    },\n    \"./src/generated/tasksController.ts\": {\n      filterEndpoints: [/tasksController/],\n    },\n    \"./src/generated/authController.ts\": {\n      filterEndpoints: [/authController/],\n    },\n  },\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/package.json",
    "content": "{\n  \"name\": \"trelly\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite --port 3000\",\n    \"build\": \"tsc -b && vite build\",\n    \"test\": \"vitest\",\n    \"preview\": \"vite preview\",\n    \"generate-api\": \"npx @rtk-query/codegen-openapi openapi-config.js\"\n  },\n  \"dependencies\": {\n    \"@emotion/react\": \"11.14.0\",\n    \"@emotion/styled\": \"11.14.0\",\n    \"@hookform/resolvers\": \"4.1.1\",\n    \"@mui/icons-material\": \"6.3.1\",\n    \"@mui/material\": \"6.3.1\",\n    \"@reduxjs/toolkit\": \"2.8.2\",\n    \"async-mutex\": \"0.5.0\",\n    \"axios\": \"1.7.9\",\n    \"react\": \"19.0.0\",\n    \"react-dom\": \"19.0.0\",\n    \"react-hook-form\": \"7.54.2\",\n    \"react-redux\": \"9.2.0\",\n    \"react-router\": \"7.6.2\",\n    \"zod\": \"3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@rtk-query/codegen-openapi\": \"2.0.0\",\n    \"@types/node\": \"22.10.6\",\n    \"@types/react\": \"19.0.7\",\n    \"@types/react-dom\": \"19.0.3\",\n    \"@vitejs/plugin-react-swc\": \"3.7.2\",\n    \"globals\": \"15.14.0\",\n    \"prettier\": \"3.4.2\",\n    \"typescript\": \"5.7.3\",\n    \"vite\": \"6.0.7\",\n    \"vitest\": \"2.1.8\"\n  }\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/app/api/baseApi.ts",
    "content": "import { createApi } from \"@reduxjs/toolkit/query/react\"\n\nimport { baseQueryWithReauth } from \"./baseQueryWithReauth.ts\"\n\nexport const baseApi = createApi({\n  reducerPath: \"baseApi\",\n  tagTypes: [\"Board\", \"Task\", \"Auth\"],\n  endpoints: () => ({}),\n  baseQuery: baseQueryWithReauth,\n})\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/app/api/baseQuery.ts",
    "content": "import { fetchBaseQuery } from \"@reduxjs/toolkit/query/react\"\n\nimport { LOCALSTORAGE_KEYS } from \"@/common/constants\"\n\nexport const baseQuery = fetchBaseQuery({\n  baseUrl: import.meta.env.VITE_BASE_URL,\n  headers: {\n    \"API-KEY\": import.meta.env.VITE_API_KEY,\n  },\n  prepareHeaders: (headers) => {\n    const accessToken = localStorage.getItem(LOCALSTORAGE_KEYS.accessToken)\n    if (accessToken) {\n      headers.set(\"Authorization\", `Bearer ${accessToken}`)\n    }\n\n    return headers\n  },\n})\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/app/api/baseQueryWithReauth.ts",
    "content": "import type { BaseQueryFn, FetchArgs, FetchBaseQueryError } from \"@reduxjs/toolkit/query/react\"\nimport { Mutex } from \"async-mutex\"\n\nimport { baseApi } from \"@/app/api/baseApi.ts\"\nimport { LOCALSTORAGE_KEYS } from \"@/common/constants\"\nimport { handleError } from \"@/common/utils\"\nimport { isTokens } from \"@/common/utils/isTokens.ts\"\n\nimport { baseQuery } from \"./baseQuery.ts\"\n\nconst mutex = new Mutex()\n\nexport const baseQueryWithReauth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (\n  args,\n  api,\n  extraOptions,\n) => {\n  await mutex.waitForUnlock() // wait until the mutex is available without locking it\n\n  let result = await baseQuery(args, api, extraOptions)\n\n  handleError(api, result) // Стандартная обработка ошибок\n\n  // refresh logic\n  if (result.error && result.error.status === 401) {\n    // checking whether the mutex is locked\n    if (!mutex.isLocked()) {\n      const release = await mutex.acquire()\n      try {\n        const refreshToken = localStorage.getItem(LOCALSTORAGE_KEYS.refreshToken)\n\n        if (!refreshToken) return result\n\n        const { data } = await baseQuery(\n          { url: \"auth/refresh\", method: \"post\", body: { refreshToken } as { refreshToken: string } },\n          api,\n          extraOptions,\n        )\n\n        if (data && isTokens(data)) {\n          localStorage.setItem(LOCALSTORAGE_KEYS.accessToken, data.accessToken)\n          localStorage.setItem(LOCALSTORAGE_KEYS.refreshToken, data.refreshToken)\n\n          result = await baseQuery(args, api, extraOptions) // Повтор запроса с новым токеном\n        } else {\n          // @ts-expect-error\n          api.dispatch(baseApi.endpoints.logout.initiate())\n        }\n      } catch (err) {\n        console.error(\"Token refresh failed:\", err)\n      } finally {\n        // release must be called once the mutex should be released again.\n        release()\n      }\n    } else {\n      // wait until the mutex is available without locking it\n      await mutex.waitForUnlock()\n      result = await baseQuery(args, api, extraOptions)\n    }\n  }\n\n  return result\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/app/model/app-slice.ts",
    "content": "import { createSlice, isFulfilled, isPending, isRejected } from \"@reduxjs/toolkit\"\n\nimport type { RequestStatus } from \"@/common/types\"\nimport { boardsApi } from \"@/features/boards/api/boardsApi.ts\"\nimport { tasksApi } from \"@/features/tasks/api/tasksApi.ts\"\n\nexport const appSlice = createSlice({\n  name: \"app\",\n  initialState: {\n    themeMode: \"light\" as ThemeMode,\n    status: \"idle\" as RequestStatus,\n    error: null as string | null,\n  },\n  selectors: {\n    selectThemeMode: (state) => state.themeMode,\n    selectAppStatus: (state) => state.status,\n    selectAppError: (state) => state.error,\n  },\n  extraReducers: (builder) => {\n    builder\n      .addMatcher(isPending, (state, action) => {\n        if (\n          boardsApi.endpoints.getBoards.matchPending(action) ||\n          tasksApi.endpoints.getBoardTasks.matchPending(action)\n        ) {\n          return\n        }\n        state.status = \"loading\"\n      })\n      .addMatcher(isFulfilled, (state) => {\n        state.status = \"succeeded\"\n      })\n      .addMatcher(isRejected, (state) => {\n        state.status = \"failed\"\n      })\n  },\n  reducers: (create) => ({\n    changeThemeModeAC: create.reducer<{ themeMode: ThemeMode }>((state, action) => {\n      state.themeMode = action.payload.themeMode\n    }),\n    setAppStatusAC: create.reducer<{ status: RequestStatus }>((state, action) => {\n      state.status = action.payload.status\n    }),\n    setAppErrorAC: create.reducer<{ error: string | null }>((state, action) => {\n      state.error = action.payload.error\n    }),\n  }),\n})\n\nexport const { selectThemeMode, selectAppStatus, selectAppError } = appSlice.selectors\nexport const { changeThemeModeAC, setAppStatusAC, setAppErrorAC } = appSlice.actions\nexport const appReducer = appSlice.reducer\n\nexport type ThemeMode = \"dark\" | \"light\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/app/model/store.ts",
    "content": "import { configureStore } from \"@reduxjs/toolkit\"\nimport { setupListeners } from \"@reduxjs/toolkit/query\"\n\nimport { baseApi } from \"@/app/api/baseApi.ts\"\n\nimport { appReducer, appSlice } from \"./app-slice.ts\"\n\nexport const store = configureStore({\n  reducer: {\n    [appSlice.name]: appReducer,\n    [baseApi.reducerPath]: baseApi.reducer,\n  },\n  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(baseApi.middleware),\n})\n\nsetupListeners(store.dispatch)\n\nexport type RootState = ReturnType<typeof store.getState>\nexport type AppDispatch = typeof store.dispatch\n\n// для возможности обращения к store в консоли браузера\n// @ts-ignore\nwindow.store = store\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/app/ui/App.module.css",
    "content": ".app {\n  display: flex;\n  flex-direction: column;\n  width: 100vw;\n}\n\n.circularProgressContainer {\n  position: fixed;\n  top: 30%;\n  text-align: center;\n  width: 100%;\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/app/ui/App.tsx",
    "content": "import { CircularProgress } from \"@mui/material\"\nimport CssBaseline from \"@mui/material/CssBaseline\"\nimport { ThemeProvider } from \"@mui/material/styles\"\n\nimport { selectThemeMode } from \"@/app/model/app-slice.ts\"\nimport { ErrorSnackbar, Header } from \"@/common/components\"\nimport { useAppSelector } from \"@/common/hooks\"\nimport { Routing } from \"@/common/routing\"\nimport { getTheme } from \"@/common/theme\"\nimport { useMeQuery } from \"@/features/auth/api/authApi.ts\"\n\nimport s from \"./App.module.css\"\n\nexport const App = () => {\n  const themeMode = useAppSelector(selectThemeMode)\n  const theme = getTheme(themeMode)\n\n  const { isLoading } = useMeQuery()\n\n  if (isLoading) {\n    return (\n      <div className={s.circularProgressContainer}>\n        <CircularProgress size={150} thickness={3} />\n      </div>\n    )\n  }\n\n  return (\n    <ThemeProvider theme={theme}>\n      <div className={s.app}>\n        <CssBaseline />\n        <Header />\n        <Routing />\n        <ErrorSnackbar />\n      </div>\n    </ThemeProvider>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/app/ui/Main.tsx",
    "content": "import Container from \"@mui/material/Container\"\nimport Grid from \"@mui/material/Grid2\"\n\nimport { CreateItemForm } from \"@/common/components/CreateItemForm/CreateItemForm.tsx\"\nimport { useAddBoardMutation } from \"@/features/boards/api/boardsApi.ts\"\nimport { Boards } from \"@/features/boards/ui/Boards/Boards.tsx\"\n\nexport const Main = () => {\n  const [mutation] = useAddBoardMutation()\n\n  const addBoardHandler = (title: string) => mutation({ title, description: \"\" })\n\n  return (\n    <Container maxWidth={\"lg\"}>\n      <Grid container sx={{ mb: \"30px\" }}>\n        <CreateItemForm onCreateItem={addBoardHandler} />\n      </Grid>\n      <Grid container spacing={4}>\n        <Boards />\n      </Grid>\n    </Container>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/actions/actions.ts",
    "content": "import { createAction } from \"@reduxjs/toolkit\"\n\nexport const clearDataAC = createAction(\"common/clearData\")\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/actions/index.ts",
    "content": "export * from \"./actions\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/CreateItemForm/CreateItemForm.tsx",
    "content": "import AddBoxIcon from \"@mui/icons-material/AddBox\"\nimport IconButton from \"@mui/material/IconButton\"\nimport TextField from \"@mui/material/TextField\"\nimport { type ChangeEvent, type KeyboardEvent, useState } from \"react\"\n\ntype Props = {\n  onCreateItem: (title: string) => void\n}\n\nexport const CreateItemForm = ({ onCreateItem }: Props) => {\n  const [title, setTitle] = useState(\"\")\n  const [error, setError] = useState<string | null>(null)\n\n  const createItemHandler = () => {\n    const trimmedTitle = title.trim()\n    if (trimmedTitle !== \"\") {\n      onCreateItem(trimmedTitle)\n      setTitle(\"\")\n    } else {\n      setError(\"Title is required\")\n    }\n  }\n\n  const changeTitleHandler = (event: ChangeEvent<HTMLInputElement>) => {\n    setTitle(event.currentTarget.value)\n    setError(null)\n  }\n\n  const createItemOnEnterHandler = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === \"Enter\") {\n      createItemHandler()\n    }\n  }\n\n  return (\n    <div>\n      <TextField\n        label={\"Enter a title\"}\n        variant={\"outlined\"}\n        value={title}\n        size={\"small\"}\n        error={!!error}\n        helperText={error}\n        onChange={changeTitleHandler}\n        onKeyDown={createItemOnEnterHandler}\n      />\n      <IconButton onClick={createItemHandler} color={\"primary\"}>\n        <AddBoxIcon />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/EditableSpan/EditableSpan.tsx",
    "content": "import TextField from \"@mui/material/TextField\"\nimport { type ChangeEvent, useState } from \"react\"\n\ntype Props = {\n  value: string\n  onChange: (title: string) => void\n  disabled?: boolean\n}\n\nexport const EditableSpan = ({ value, onChange, disabled }: Props) => {\n  const [title, setTitle] = useState(value)\n  const [isEditMode, setIsEditMode] = useState(false)\n\n  const turnOnEditMode = () => {\n    if (disabled) return\n    setIsEditMode(true)\n  }\n\n  const turnOffEditMode = () => {\n    setIsEditMode(false)\n    onChange(title)\n  }\n\n  const changeTitle = (event: ChangeEvent<HTMLInputElement>) => {\n    setTitle(event.currentTarget.value)\n  }\n\n  return (\n    <>\n      {isEditMode ? (\n        <TextField\n          variant={\"outlined\"}\n          value={title}\n          size={\"small\"}\n          onChange={changeTitle}\n          onBlur={turnOffEditMode}\n          autoFocus\n        />\n      ) : (\n        <span onDoubleClick={turnOnEditMode}>{value}</span>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/ErrorSnackbar/ErrorSnackbar.tsx",
    "content": "import Alert from \"@mui/material/Alert\"\nimport Snackbar from \"@mui/material/Snackbar\"\nimport { SyntheticEvent } from \"react\"\n\nimport { selectAppError, setAppErrorAC } from \"@/app/model/app-slice.ts\"\nimport { useAppDispatch, useAppSelector } from \"@/common/hooks\"\n\nexport const ErrorSnackbar = () => {\n  const error = useAppSelector(selectAppError)\n\n  const dispatch = useAppDispatch()\n\n  const handleClose = (_: SyntheticEvent | Event, reason?: string) => {\n    if (reason === \"clickaway\") {\n      return\n    }\n\n    dispatch(setAppErrorAC({ error: null }))\n  }\n\n  return (\n    <Snackbar open={error !== null} autoHideDuration={6000} onClose={handleClose}>\n      <Alert onClose={handleClose} severity=\"error\" variant=\"filled\" sx={{ width: \"100%\" }}>\n        {error}\n      </Alert>\n    </Snackbar>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/Header/Header.tsx",
    "content": "import MenuIcon from \"@mui/icons-material/Menu\"\nimport AppBar from \"@mui/material/AppBar\"\nimport Container from \"@mui/material/Container\"\nimport IconButton from \"@mui/material/IconButton\"\nimport LinearProgress from \"@mui/material/LinearProgress\"\nimport Switch from \"@mui/material/Switch\"\nimport Toolbar from \"@mui/material/Toolbar\"\n\nimport { changeThemeModeAC, selectAppStatus, selectThemeMode } from \"@/app/model/app-slice.ts\"\nimport { NavButton } from \"@/common/components/NavButton/NavButton\"\nimport { useAppDispatch, useAppSelector } from \"@/common/hooks\"\nimport { containerSx } from \"@/common/styles\"\nimport { getTheme } from \"@/common/theme\"\nimport { UserBlock } from \"@/features/auth/ui/UserBlock/UserBlock.tsx\"\n\nexport const Header = () => {\n  const themeMode = useAppSelector(selectThemeMode)\n  const status = useAppSelector(selectAppStatus)\n\n  const dispatch = useAppDispatch()\n\n  const theme = getTheme(themeMode)\n\n  const changeMode = () => {\n    dispatch(changeThemeModeAC({ themeMode: themeMode === \"light\" ? \"dark\" : \"light\" }))\n  }\n\n  return (\n    <AppBar position=\"static\" sx={{ mb: \"30px\" }}>\n      <Toolbar>\n        <Container maxWidth={\"lg\"} sx={containerSx}>\n          <IconButton color=\"inherit\">\n            <MenuIcon />\n          </IconButton>\n          <div style={{ display: \"flex\" }}>\n            <UserBlock />\n            <NavButton background={theme.palette.primary.dark}>Faq</NavButton>\n            <Switch color={\"default\"} onChange={changeMode} />\n          </div>\n        </Container>\n      </Toolbar>\n      {status === \"loading\" && <LinearProgress />}\n    </AppBar>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/NavButton/NavButton.ts",
    "content": "import Button from \"@mui/material/Button\"\nimport { styled } from \"@mui/material/styles\"\n\ntype Props = {\n  background?: string\n}\n\nexport const NavButton = styled(Button)<Props>(({ background, theme }) => ({\n  minWidth: \"110px\",\n  fontWeight: \"bold\",\n  boxShadow: `0 0 0 2px ${theme.palette.primary.dark}, 4px 4px 0 0 ${theme.palette.primary.dark}`,\n  borderRadius: \"2px\",\n  textTransform: \"capitalize\",\n  margin: \"0 10px\",\n  padding: \"8px 24px\",\n  color: theme.palette.primary.contrastText,\n  background: background || theme.palette.primary.light,\n}))\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/PageNotFound/PageNotFound.module.css",
    "content": ".title {\n  text-align: center;\n  font-size: 250px;\n  margin: 0;\n}\n\n.subtitle {\n  text-align: center;\n  font-size: 50px;\n  margin: 0;\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/PageNotFound/PageNotFound.tsx",
    "content": "import Button from \"@mui/material/Button\"\nimport Container from \"@mui/material/Container\"\nimport { Link } from \"react-router\"\n\nimport { Path } from \"@/common/routing\"\n\nimport styles from \"./PageNotFound.module.css\"\n\nexport const PageNotFound = () => (\n  <Container sx={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\" }}>\n    <h1 className={styles.title}>404</h1>\n    <h2 className={styles.subtitle}>page not found</h2>\n    <Button variant=\"contained\" component={Link} to={Path.Main} sx={{ width: \"330px\", mt: \"20px\" }}>\n      Вернуться на главную\n    </Button>\n  </Container>\n)\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/ProtectedRoute/ProtectedRoute.tsx",
    "content": "import type { ReactNode } from \"react\"\nimport { Navigate, Outlet } from \"react-router\"\n\nimport { Path } from \"@/common/routing\"\n\ntype Props = {\n  isAllowed: boolean\n  children?: ReactNode\n  redirectPath?: string\n}\n\nexport const ProtectedRoute = ({ children, isAllowed, redirectPath = Path.Main }: Props) => {\n  if (!isAllowed) {\n    return <Navigate to={redirectPath} />\n  }\n  return children || <Outlet />\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/components/index.ts",
    "content": "export { CreateItemForm } from \"./CreateItemForm/CreateItemForm\"\nexport { EditableSpan } from \"./EditableSpan/EditableSpan\"\nexport { ErrorSnackbar } from \"./ErrorSnackbar/ErrorSnackbar\"\nexport { Header } from \"./Header/Header\"\nexport { NavButton } from \"./NavButton/NavButton\"\nexport { PageNotFound } from \"./PageNotFound/PageNotFound\"\nexport { ProtectedRoute } from \"./ProtectedRoute/ProtectedRoute\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/constants/constants.ts",
    "content": "export const PAGE_SIZE = 4\n\nexport const LOCALSTORAGE_KEYS = {\n  refreshToken: \"trelly-refresh-token\",\n  accessToken: \"trelly-access-token\",\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/constants/index.ts",
    "content": "export * from \"./constants\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/enums/enums.ts",
    "content": "export enum TaskStatus {\n  New = 0,\n  InProgress = 1,\n  Completed = 2,\n  Draft = 3,\n}\n\nexport enum TaskPriority {\n  Low = 0,\n  Middle = 1,\n  Hi = 2,\n  Urgently = 3,\n  Later = 4,\n}\n\nexport enum ResultCode {\n  Success = 0,\n  Error = 1,\n  CaptchaError = 10,\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/enums/index.ts",
    "content": "export * from \"./enums\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/hooks/index.ts",
    "content": "export { useAppDispatch } from \"./useAppDispatch\"\nexport { useAppSelector } from \"./useAppSelector\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/hooks/useAppDispatch.ts",
    "content": "import { useDispatch } from \"react-redux\"\n\nimport type { AppDispatch } from \"@/app/model/store.ts\"\n\nexport const useAppDispatch = useDispatch.withTypes<AppDispatch>()\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/hooks/useAppSelector.ts",
    "content": "import { useSelector } from \"react-redux\"\n\nimport type { RootState } from \"@/app/model/store.ts\"\n\nexport const useAppSelector = useSelector.withTypes<RootState>()\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/instance/index.ts",
    "content": "export { instance } from \"./instance\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/instance/instance.ts",
    "content": "import axios from \"axios\"\n\nimport { AUTH_TOKEN } from \"@/common/constants\"\n\nexport const instance = axios.create({\n  baseURL: import.meta.env.VITE_BASE_URL,\n  headers: {\n    \"API-KEY\": import.meta.env.VITE_API_KEY,\n  },\n})\n\ninstance.interceptors.request.use(function (config) {\n  config.headers[\"Authorization\"] = `Bearer ${localStorage.getItem(AUTH_TOKEN)}`\n  return config\n})\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/routing/Routing.tsx",
    "content": "import { Route, Routes } from \"react-router\"\n\nimport { Main } from \"@/app/ui/Main.tsx\"\nimport { PageNotFound, ProtectedRoute } from \"@/common/components\"\nimport { useMeQuery } from \"@/features/auth/api/authApi.ts\"\nimport { Login } from \"@/features/auth/ui/Login/Login\"\n\nimport { OAuthCallback } from \"../../features/auth/ui/OAuthCallback/OAuthCallback.tsx\"\n\nexport const Path = {\n  Main: \"/\",\n  Login: \"/login\",\n  OAuthRedirect: \"/oauth/callback\",\n  NotFound: \"*\",\n} as const\n\nexport const Routing = () => {\n  const { data } = useMeQuery()\n\n  return (\n    <Routes>\n      <Route path={Path.Main} element={<Main />} />\n      <Route element={<ProtectedRoute isAllowed={!data} />}>\n        <Route path={Path.Login} element={<Login />} />\n      </Route>\n      <Route path={Path.OAuthRedirect} element={<OAuthCallback />} />\n      <Route path={Path.NotFound} element={<PageNotFound />} />\n    </Routes>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/routing/index.ts",
    "content": "export * from \"./Routing\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/styles/container.styles.ts",
    "content": "import { SxProps } from \"@mui/material\"\n\nexport const containerSx: SxProps = {\n  display: \"flex\",\n  justifyContent: \"space-between\",\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/styles/index.ts",
    "content": "export { containerSx } from \"./container.styles\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/theme/index.ts",
    "content": "export { getTheme } from \"./theme\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/theme/theme.ts",
    "content": "import { createTheme } from \"@mui/material/styles\"\n\nimport type { ThemeMode } from \"@/app/model/app-slice.ts\"\n\nexport const getTheme = (themeMode: ThemeMode) => {\n  return createTheme({\n    palette: {\n      mode: themeMode,\n      primary: {\n        main: \"#087EA4\",\n      },\n    },\n  })\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/types/index.ts",
    "content": "export * from \"./types\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/types/types.ts",
    "content": "export type FieldError = {\n  error: string\n  field: string\n}\n\nexport type BaseResponse<T = {}> = {\n  data: T\n  resultCode: number\n  messages: string[]\n  fieldsErrors: FieldError[]\n}\n\nexport type RequestStatus = \"idle\" | \"loading\" | \"succeeded\" | \"failed\"\n\nexport type Meta = {\n  page: number\n  pageSize: number\n  totalCount: number\n  pagesCount: number\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/utils/createAppSlice.ts",
    "content": "import { asyncThunkCreator, buildCreateSlice } from \"@reduxjs/toolkit\"\n\nexport const createAppSlice = buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/utils/handleError.ts",
    "content": "import { BaseQueryApi, FetchBaseQueryError, FetchBaseQueryMeta, QueryReturnValue } from \"@reduxjs/toolkit/query/react\"\n\nimport { setAppErrorAC } from \"@/app/model/app-slice.ts\"\n\nimport { isErrorWithMessage } from \"./isErrorWithMessage\"\n\nexport const handleError = (\n  api: BaseQueryApi,\n  result: QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>,\n) => {\n  let error = \"Some error occurred\"\n\n  if (result.error) {\n    switch (result.error.status) {\n      case \"FETCH_ERROR\":\n      case \"PARSING_ERROR\":\n      case \"CUSTOM_ERROR\":\n        error = result.error.error\n        break\n      case 401:\n        return // Не показываем на фронте 401 ошибку\n      case 403:\n        error = \"403 Forbidden Error. Check API-KEY\"\n        break\n      case 400:\n      case 500:\n        if (isErrorWithMessage(result.error.data)) {\n          error = result.error.data.message\n        } else {\n          error = JSON.stringify(result.error.data)\n        }\n        break\n      default:\n        error = JSON.stringify(result.error)\n        break\n    }\n    api.dispatch(setAppErrorAC({ error }))\n  }\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/utils/index.ts",
    "content": "export { createAppSlice } from \"./createAppSlice\"\nexport { handleError } from \"./handleError\"\nexport { isErrorWithMessage } from \"./isErrorWithMessage\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/utils/isErrorWithMessage.ts",
    "content": "export function isErrorWithMessage(error: unknown): error is { message: string } {\n  return (\n    typeof error === \"object\" && // Проверяем, что error – это объект\n    error != null && // Убеждаемся, что это не null\n    \"message\" in error && // Проверяем, что у объекта есть свойство 'message'\n    typeof (error as any).message === \"string\" // Убеждаемся, что это строка\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/common/utils/isTokens.ts",
    "content": "export const isTokens = (data: unknown): data is { accessToken: string; refreshToken: string } => {\n  return typeof data === \"object\" && data !== null && \"accessToken\" in data && \"refreshToken\" in data\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/auth/api/authApi.ts",
    "content": "import { baseApi } from \"@/app/api/baseApi.ts\"\nimport { LOCALSTORAGE_KEYS } from \"@/common/constants\"\n\nimport type { LoginArgs, OAuthResponse } from \"./authApi.types.ts\"\n\nexport const authApi = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    me: build.query<{ userId: string; login: string }, void>({\n      query: () => \"auth/me\",\n      providesTags: [\"Auth\"],\n    }),\n    login: build.mutation<OAuthResponse, LoginArgs>({\n      async onQueryStarted(_args, { queryFulfilled }) {\n        try {\n          const { data } = await queryFulfilled\n          if (!data) return\n          localStorage.setItem(LOCALSTORAGE_KEYS.refreshToken, data.refreshToken)\n          localStorage.setItem(LOCALSTORAGE_KEYS.accessToken, data.accessToken)\n        } catch (err) {\n          console.log(err)\n        }\n      },\n      query: (body) => ({ url: \"auth/login\", method: \"post\", body }),\n      invalidatesTags: [\"Auth\"],\n    }),\n    logout: build.mutation<void, void>({\n      async onQueryStarted(_args, { queryFulfilled, dispatch }) {\n        await queryFulfilled\n        localStorage.removeItem(LOCALSTORAGE_KEYS.accessToken)\n        localStorage.removeItem(LOCALSTORAGE_KEYS.refreshToken)\n        dispatch(baseApi.util.resetApiState())\n      },\n      query: () => {\n        const refreshToken = localStorage.getItem(LOCALSTORAGE_KEYS.refreshToken)\n        return { url: \"auth/logout\", method: \"post\", body: { refreshToken } }\n      },\n    }),\n  }),\n})\n\nexport const { useLoginMutation, useLogoutMutation, useMeQuery } = authApi\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/auth/api/authApi.types.ts",
    "content": "// Response\nexport type OAuthResponse = {\n  refreshToken: string\n  accessToken: string\n}\n\n// Arguments\nexport type LoginArgs = {\n  code: string\n  redirectUri: string\n  accessTokenTTL: string // e.g. \"3m\"\n  rememberMe: boolean\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/auth/lib/schemas/index.ts",
    "content": "export * from \"./loginSchema\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/auth/lib/schemas/loginSchema.ts",
    "content": "import { z } from \"zod\"\n\nexport const loginSchema = z.object({\n  email: z.string().min(1, { message: \"Email is required\" }).email({ message: \"Incorrect email address\" }),\n  password: z\n    .string()\n    .min(1, { message: \"Password is required\" })\n    .min(3, { message: \"Password must be at least 3 characters long\" }),\n  rememberMe: z.boolean(),\n})\n\nexport type Inputs = z.infer<typeof loginSchema>\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/auth/ui/Login/Login.module.css",
    "content": ".wrapper {\n  width: 500px;\n  margin: 0 auto;\n  text-align: center;\n}\n\n.container {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/auth/ui/Login/Login.tsx",
    "content": "import Button from \"@mui/material/Button\"\n\nimport { Path } from \"@/common/routing\"\nimport { useLoginMutation } from \"@/features/auth/api/authApi\"\n\nimport s from \"./Login.module.css\"\n\nexport const Login = () => {\n  const [mutate] = useLoginMutation()\n\n  const loginHandler = () => {\n    const redirectUri = import.meta.env.VITE_DOMAIN_ADDRESS + Path.OAuthRedirect\n    const url = `${import.meta.env.VITE_BASE_URL}/auth/oauth-redirect?callbackUrl=${redirectUri}`\n\n    window.open(url, \"oauthPopup\", \"width=500,height=600\")\n\n    const receiveMessage = async (event: MessageEvent) => {\n      if (event.origin !== import.meta.env.VITE_DOMAIN_ADDRESS) return\n\n      const { code } = event.data\n      if (!code) return\n\n      window.removeEventListener(\"message\", receiveMessage)\n      mutate({ code, accessTokenTTL: \"3m\", redirectUri, rememberMe: false })\n    }\n\n    window.addEventListener(\"message\", receiveMessage)\n  }\n\n  return (\n    <div className={s.wrapper}>\n      <h1>Trelly</h1>\n      <div className={s.container}>\n        <Button variant=\"contained\" color=\"secondary\" component={\"a\"} href={Path.Main}>\n          Сontinue without Sign In\n        </Button>\n        <Button variant=\"contained\" color=\"primary\" onClick={loginHandler}>\n          Login with APIHUB\n        </Button>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/auth/ui/OAuthCallback/OAuthCallback.tsx",
    "content": "import { useEffect } from \"react\"\n\nexport const OAuthCallback = () => {\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const code = url.searchParams.get(\"code\") // или code/state, если flow другой\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, \"*\") // Лучше заменить \"*\" на точный origin\n    }\n\n    window.close()\n  }, [])\n\n  return <p>Logging you in...</p>\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/auth/ui/UserBlock/UserBlock.tsx",
    "content": "import { useNavigate } from \"react-router\"\n\nimport { NavButton } from \"@/common/components\"\nimport { Path } from \"@/common/routing\"\nimport { useLogoutMutation, useMeQuery } from \"@/features/auth/api/authApi\"\n\nexport const UserBlock = () => {\n  const navigate = useNavigate()\n\n  const { data: user } = useMeQuery()\n\n  const [logout] = useLogoutMutation()\n\n  const handleLogout = () => logout()\n\n  const handleLogin = () => navigate(Path.Login)\n\n  if (!user) {\n    return (\n      <div>\n        <NavButton onClick={handleLogin}>Login</NavButton>\n      </div>\n    )\n  }\n\n  return (\n    <div>\n      <span>name: {user.login}</span>\n      <NavButton onClick={handleLogout}>Sign out</NavButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/api/boardsApi.ts",
    "content": "import { baseApi } from \"@/app/api/baseApi.ts\"\n\nimport type { Board, BoardsResponse, DomainBoardResponse, UpdateBoardArgs } from \"./boardsApi.types.ts\"\n\nexport const boardsApi = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    getBoards: build.query<DomainBoardResponse, void>({\n      query: () => \"boards\",\n      transformResponse: (res: BoardsResponse): DomainBoardResponse => {\n        return { ...res, data: res.data.map((board) => ({ ...board, filter: \"all\" })) }\n      },\n      providesTags: [\"Board\"],\n    }),\n    addBoard: build.mutation<{ data: Board }, { title: string; description?: string }>({\n      query: (body) => ({ url: \"boards\", method: \"POST\", body }),\n      invalidatesTags: [\"Board\"],\n    }),\n    removeBoard: build.mutation<void, string, { revertOnError: true }>({\n      query: (id) => ({ url: `boards/${id}`, method: \"DELETE\" }),\n      invalidatesTags: [\"Board\"],\n      onQueryStarted: async (boardId, { dispatch, queryFulfilled }) => {\n        const patchResult = dispatch(\n          boardsApi.util.updateQueryData(\"getBoards\", undefined, (state) => {\n            const index = state.data.findIndex((board) => board.id === boardId)\n            if (index !== -1) state.data.splice(index, 1)\n          }),\n        )\n        try {\n          await queryFulfilled\n        } catch {\n          patchResult.undo()\n        }\n      },\n    }),\n    updateBoardTitle: build.mutation<void, UpdateBoardArgs>({\n      query: ({ id, title, isImportant, description }) => ({\n        url: `boards/${id}`,\n        method: \"PUT\",\n        body: { title, isImportant, description },\n      }),\n      invalidatesTags: [\"Board\"],\n    }),\n  }),\n})\n\nexport const { useGetBoardsQuery, useAddBoardMutation, useRemoveBoardMutation, useUpdateBoardTitleMutation } = boardsApi\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/api/boardsApi.types.ts",
    "content": "import { z } from \"zod\"\n\nimport type { Meta } from \"@/common/types\"\n\nexport type FilterValues = \"all\" | \"active\" | \"completed\"\n\nexport type BoardsResponse = {\n  data: Board[]\n  meta: Meta\n}\n\nexport type Board = {\n  id: string\n  type: \"boards\"\n  attributes: BoardAttributes\n}\n\nexport type DomainBoard = { filter: FilterValues } & Board\n\nexport type DomainBoardResponse = {\n  data: DomainBoard[]\n  meta: Meta\n}\n\nexport type BoardAttributes = {\n  title: string\n  description: string\n  addedAt: string\n  images: Images\n  isImportant: boolean\n  order: number\n  updatedAt: string\n}\n\n// TODO: проверить реально ли такой тип возвращает бекенд\nexport type Images = {\n  main: Cover[]\n}\n\nexport type Cover = {\n  type: \"original\" | \"medium\" | \"thumbnail\"\n  width: number\n  height: number\n  fileSize: number\n  url: string\n}\n\n// Arguments\nexport type UpdateBoardArgs = {\n  id: string\n  title: string\n  description: string\n  isImportant: boolean\n}\n\n// 👴 old with zod\nexport const TodolistSchema = z.object({\n  id: z.string(),\n  title: z.string(),\n  addedDate: z.string(),\n  order: z.number(),\n})\n\nexport type Todolist = z.infer<typeof TodolistSchema>\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/lib/utils/createTaskModel.ts",
    "content": "import type { Task, UpdateTaskModel } from \"@/features/tasks/api/tasksApi.types.ts\"\n\nexport const createTaskModel = (task: Task, domainModel: Partial<UpdateTaskModel>): UpdateTaskModel => ({\n  status: task.attributes.status,\n  title: task.attributes.title,\n  priority: task.attributes.priority,\n  startDate: task.attributes.startDate,\n  // TODO: deadline дублируется с title на бекенде\n  deadline: \"2025-06-26T11:40:34.962Z\",\n  // TODO: description не возвращается при GET запросе\n  description: \"\",\n  ...domainModel,\n})\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/lib/utils/index.ts",
    "content": "export * from \"./createTaskModel\"\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/ui/Boards/BoardItem/BoardItem.tsx",
    "content": "import { CreateItemForm } from \"@/common/components/CreateItemForm/CreateItemForm.tsx\"\nimport type { DomainBoard } from \"@/features/boards/api/boardsApi.types.ts\"\nimport { BoardTitle } from \"@/features/boards/ui/Boards/BoardItem/BoardTitle/BoardTitle.tsx\"\nimport { FilterButtons } from \"@/features/boards/ui/Boards/BoardItem/FilterButtons/FilterButtons.tsx\"\nimport { useAddTaskMutation } from \"@/features/tasks/api/tasksApi.ts\"\n\nimport { Tasks } from \"../../../../tasks/ui/Tasks/Tasks.tsx\"\n\ntype Props = {\n  board: DomainBoard\n}\n\nexport const BoardItem = ({ board }: Props) => {\n  const [mutation] = useAddTaskMutation()\n\n  const createTask = (title: string) => mutation({ boardId: board.id, title })\n\n  return (\n    <>\n      <BoardTitle board={board} />\n      <CreateItemForm onCreateItem={createTask} />\n      <Tasks board={board} />\n      <FilterButtons board={board} />\n    </>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/ui/Boards/BoardItem/BoardTitle/BoardTitle.module.css",
    "content": ".container {\n  display: flex;\n  align-items: center;\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/ui/Boards/BoardItem/BoardTitle/BoardTitle.tsx",
    "content": "import DeleteIcon from \"@mui/icons-material/Delete\"\nimport IconButton from \"@mui/material/IconButton\"\n\nimport { EditableSpan } from \"@/common/components\"\nimport { useRemoveBoardMutation, useUpdateBoardTitleMutation } from \"@/features/boards/api/boardsApi.ts\"\n\nimport type { DomainBoard, UpdateBoardArgs } from \"../../../../api/boardsApi.types.ts\"\nimport s from \"./BoardTitle.module.css\"\n\ntype Props = {\n  board: DomainBoard\n}\n\nexport const BoardTitle = ({ board }: Props) => {\n  const { id, attributes } = board\n\n  const [removeBoard] = useRemoveBoardMutation()\n  const [updateBoardTitle] = useUpdateBoardTitleMutation()\n\n  const changeBoardTitle = (title: string) => {\n    const payload: UpdateBoardArgs = {\n      title,\n      id: board.id,\n      isImportant: board.attributes.isImportant,\n      description: board.attributes.description,\n    }\n    updateBoardTitle(payload)\n  }\n\n  return (\n    <div className={s.container}>\n      <h3>\n        <EditableSpan value={attributes.title} onChange={changeBoardTitle} />\n      </h3>\n      <IconButton onClick={() => removeBoard(id)}>\n        <DeleteIcon />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/ui/Boards/BoardItem/FilterButtons/FilterButtons.tsx",
    "content": "import Box from \"@mui/material/Box\"\nimport Button from \"@mui/material/Button\"\n\nimport { useAppDispatch } from \"@/common/hooks\"\nimport { containerSx } from \"@/common/styles\"\nimport { boardsApi } from \"@/features/boards/api/boardsApi.ts\"\n\nimport type { DomainBoard, FilterValues } from \"../../../../api/boardsApi.types.ts\"\n\ntype Props = {\n  board: DomainBoard\n}\n\nexport const FilterButtons = ({ board }: Props) => {\n  const { id, filter } = board\n\n  const dispatch = useAppDispatch()\n\n  const changeFilter = (filter: FilterValues) => {\n    dispatch(\n      boardsApi.util.updateQueryData(\"getBoards\", undefined, (state) => {\n        const board = state.data.find((b) => b.id === id)\n        if (board) {\n          board.filter = filter\n        }\n      }),\n    )\n  }\n\n  return (\n    <Box sx={containerSx}>\n      <Button variant={filter === \"all\" ? \"outlined\" : \"text\"} color={\"inherit\"} onClick={() => changeFilter(\"all\")}>\n        All\n      </Button>\n      <Button\n        variant={filter === \"active\" ? \"outlined\" : \"text\"}\n        color={\"primary\"}\n        onClick={() => changeFilter(\"active\")}\n      >\n        Active\n      </Button>\n      <Button\n        variant={filter === \"completed\" ? \"outlined\" : \"text\"}\n        color={\"secondary\"}\n        onClick={() => changeFilter(\"completed\")}\n      >\n        Completed\n      </Button>\n    </Box>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/ui/Boards/BoardSkeleton/BoardSkeleton.module.css",
    "content": ".common {\n  display: flex;\n  justify-content: space-between;\n}\n\n.container {\n  width: 305px;\n  padding: 10px 20px;\n}\n\n.title {\n  display: flex;\n  gap: 15px;\n  align-items: center;\n}\n\n.createItemForm {\n  composes: common;\n  align-items: center;\n}\n\n.tasks {\n  composes: common;\n  gap: 15px;\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/ui/Boards/BoardSkeleton/BoardSkeleton.tsx",
    "content": "import Box from \"@mui/material/Box\"\nimport Paper from \"@mui/material/Paper\"\nimport Skeleton from \"@mui/material/Skeleton\"\n\nimport { containerSx } from \"@/common/styles\"\n\nimport styles from \"./BoardSkeleton.module.css\"\n\nexport const BoardSkeleton = () => (\n  <Paper className={styles.container}>\n    <div className={styles.title}>\n      <Skeleton width={150} height={50} />\n      <Skeleton width={20} height={40} />\n    </div>\n    <div className={styles.createItemForm}>\n      <Skeleton width={230} height={60} />\n      <Skeleton width={20} height={40} />\n    </div>\n    {Array(4)\n      .fill(null)\n      .map((_, id) => (\n        <Box key={id} sx={containerSx}>\n          <div className={styles.tasks}>\n            <Skeleton width={20} height={40} />\n            <Skeleton width={150} height={40} />\n          </div>\n          <Skeleton width={20} height={40} />\n        </Box>\n      ))}\n    <Box sx={containerSx}>\n      {Array(3)\n        .fill(null)\n        .map((_, id) => (\n          <Skeleton key={id} width={80} height={60} />\n        ))}\n    </Box>\n  </Paper>\n)\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/boards/ui/Boards/Boards.tsx",
    "content": "import Box from \"@mui/material/Box\"\nimport Grid from \"@mui/material/Grid2\"\nimport Paper from \"@mui/material/Paper\"\n\nimport { containerSx } from \"@/common/styles\"\nimport { useGetBoardsQuery } from \"@/features/boards/api/boardsApi.ts\"\nimport { BoardItem } from \"@/features/boards/ui/Boards/BoardItem/BoardItem.tsx\"\nimport { BoardSkeleton } from \"@/features/boards/ui/Boards/BoardSkeleton/BoardSkeleton.tsx\"\n\nexport const Boards = () => {\n  const { data, isLoading } = useGetBoardsQuery()\n\n  if (isLoading) {\n    return (\n      <Box sx={containerSx} style={{ gap: \"32px\" }}>\n        {Array(3)\n          .fill(null)\n          .map((_, id) => (\n            <BoardSkeleton key={id} />\n          ))}\n      </Box>\n    )\n  }\n\n  return (\n    <>\n      {data?.data.map((board) => (\n        <Grid key={board.id}>\n          <Paper sx={{ p: \"0 20px 20px 20px\" }}>\n            <BoardItem board={board} />\n          </Paper>\n        </Grid>\n      ))}\n    </>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/tasks/api/tasksApi.ts",
    "content": "import { baseApi } from \"@/app/api/baseApi.ts\"\nimport { PAGE_SIZE } from \"@/common/constants\"\n\nimport type { AddTaskResponse, GetBoardTasksResponse, UpdateTaskModel, UpdateTaskResponse } from \"./tasksApi.types.ts\"\n\nexport const tasksApi = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    getBoardTasks: build.query<GetBoardTasksResponse, { boardId: string; params: { page: number } }>({\n      query: ({ boardId, params }) => ({ url: `boards/${boardId}/tasks`, params: { ...params, count: PAGE_SIZE } }),\n      providesTags: (_result, _error, { boardId }) => [{ type: \"Task\", id: boardId }],\n    }),\n    addTask: build.mutation<AddTaskResponse, { boardId: string; title: string }>({\n      query: ({ boardId, title }) => ({ url: `boards/${boardId}/tasks`, method: \"POST\", body: { title } }),\n      invalidatesTags: (_result, _error, { boardId }) => [{ type: \"Task\", id: boardId }],\n    }),\n    removeTask: build.mutation<void, { boardId: string; taskId: string }>({\n      query: ({ boardId, taskId }) => ({ url: `boards/${boardId}/tasks/${taskId}`, method: \"DELETE\" }),\n      invalidatesTags: (_result, _error, { boardId }) => [{ type: \"Task\", id: boardId }],\n    }),\n    updateTask: build.mutation<\n      UpdateTaskResponse,\n      { boardId: string; taskId: string; model: UpdateTaskModel; page: number }\n    >({\n      query: ({ boardId, taskId, model }) => ({ url: `boards/${boardId}/tasks/${taskId}`, method: \"PUT\", body: model }),\n      onQueryStarted: async ({ taskId, model, boardId, page }, { dispatch, queryFulfilled }) => {\n        const patchResult = dispatch(\n          tasksApi.util.updateQueryData(\"getBoardTasks\", { boardId, params: { page } }, (state) => {\n            const index = state.data.findIndex((task) => task.id === taskId)\n            if (index !== -1) {\n              state.data[index] = { ...state.data[index], ...model }\n            }\n          }),\n        )\n\n        try {\n          await queryFulfilled\n        } catch (err) {\n          patchResult.undo()\n        }\n      },\n      invalidatesTags: (_result, _error, { boardId }) => [{ type: \"Task\", id: boardId }],\n    }),\n  }),\n})\n\nexport const { useGetBoardTasksQuery, useAddTaskMutation, useRemoveTaskMutation, useUpdateTaskMutation } = tasksApi\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/tasks/api/tasksApi.types.ts",
    "content": "import { z } from \"zod\"\n\nimport { TaskPriority, TaskStatus } from \"@/common/enums\"\nimport type { Meta } from \"@/common/types\"\n\nexport type GetBoardTasksResponse = {\n  data: Task[]\n  meta: Meta\n}\n\nexport type AddTaskResponse = {\n  data: {\n    id: string\n    type: \"tasks\"\n    attributes: TaskAttributes & {\n      description: string | null\n      boardId: string\n      boardTitle: string\n    }\n  }\n}\n\nexport type UpdateTaskResponse = {\n  id: string\n  addedAt: string\n  boardId: string\n  deadline: string\n  deletedAt: string | null\n  description: string\n  order: number\n  priority: number\n  startDate: string | null\n  status: number\n  title: string\n  updatedAt: string\n  version: number\n  board: {\n    id: string\n    addedAt: string\n    deletedAt: string | null\n    description: string\n    isImportant: boolean\n    order: number\n    title: string\n    updatedAt: string\n    userId: string\n    version: number\n  }\n}\n\nexport type Task = {\n  id: string\n  type: \"tasks\"\n  attributes: TaskAttributes\n}\n\nexport type TaskAttributes = {\n  addedAt: string\n  attachments: string[]\n  deadline: string\n  order: number\n  priority: TaskPriority\n  startDate: string | null\n  status: TaskStatus\n  title: string\n  updatedAt: string\n}\n\n// 👴 old with zod\nexport const DomainTaskSchema = z.object({\n  description: z.string().nullable(),\n  title: z.string(),\n  status: z.nativeEnum(TaskStatus),\n  priority: z.nativeEnum(TaskPriority),\n  startDate: z.string().nullable(),\n  deadline: z.string().nullable(),\n  id: z.string(),\n  todoListId: z.string(),\n  order: z.number(),\n  addedDate: z.string(),\n})\n\nexport type DomainTask = z.infer<typeof DomainTaskSchema>\n\nexport type UpdateTaskModel = {\n  title: string\n  description: string\n  status: TaskStatus\n  priority: TaskPriority\n  startDate: string | null\n  deadline: string | null\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/tasks/ui/Tasks/TaskItem/TaskItem.styles.ts",
    "content": "import { SxProps } from \"@mui/material\"\n\nexport const getListItemSx = (isDone: boolean): SxProps => ({\n  p: 0,\n  justifyContent: \"space-between\",\n  opacity: isDone ? 0.5 : 1,\n})\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/tasks/ui/Tasks/TaskItem/TaskItem.tsx",
    "content": "import DeleteIcon from \"@mui/icons-material/Delete\"\nimport Checkbox from \"@mui/material/Checkbox\"\nimport IconButton from \"@mui/material/IconButton\"\nimport ListItem from \"@mui/material/ListItem\"\nimport type { ChangeEvent } from \"react\"\n\nimport { EditableSpan } from \"@/common/components\"\nimport { TaskStatus } from \"@/common/enums\"\nimport type { DomainBoard } from \"@/features/boards/api/boardsApi.types.ts\"\nimport { createTaskModel } from \"@/features/boards/lib/utils\"\nimport { useRemoveTaskMutation, useUpdateTaskMutation } from \"@/features/tasks/api/tasksApi.ts\"\nimport type { Task } from \"@/features/tasks/api/tasksApi.types.ts\"\n\nimport { getListItemSx } from \"./TaskItem.styles.ts\"\n\ntype Props = {\n  task: Task\n  board: DomainBoard\n  page: number\n}\n\nexport const TaskItem = ({ task, board, page }: Props) => {\n  const [removeTask] = useRemoveTaskMutation()\n  const [updateTask] = useUpdateTaskMutation()\n\n  const deleteTask = () => {\n    removeTask({ boardId: board.id, taskId: task.id })\n  }\n\n  const changeTaskStatus = (e: ChangeEvent<HTMLInputElement>) => {\n    const status = e.currentTarget.checked ? TaskStatus.Completed : TaskStatus.New\n    const model = createTaskModel(task, { status })\n    updateTask({ taskId: task.id, boardId: board.id, model, page })\n  }\n\n  const changeTaskTitle = (title: string) => {\n    const model = createTaskModel(task, { title })\n    updateTask({ taskId: task.id, boardId: board.id, model, page })\n  }\n\n  const isTaskCompleted = task.attributes.status === TaskStatus.Completed\n\n  return (\n    <ListItem sx={getListItemSx(isTaskCompleted)}>\n      <div>\n        <Checkbox checked={isTaskCompleted} onChange={changeTaskStatus} />\n        <EditableSpan value={task.attributes.title} onChange={changeTaskTitle} />\n      </div>\n      <IconButton onClick={deleteTask}>\n        <DeleteIcon />\n      </IconButton>\n    </ListItem>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/tasks/ui/Tasks/Tasks.tsx",
    "content": "import List from \"@mui/material/List\"\nimport { useState } from \"react\"\n\nimport { TaskStatus } from \"@/common/enums\"\nimport type { DomainBoard } from \"@/features/boards/api/boardsApi.types.ts\"\nimport { useGetBoardTasksQuery } from \"@/features/tasks/api/tasksApi.ts\"\nimport { TasksPagination } from \"@/features/tasks/ui/Tasks/TasksPagination/TasksPagination.tsx\"\n\nimport { TaskItem } from \"./TaskItem/TaskItem.tsx\"\nimport { TasksSkeleton } from \"./TasksSkeleton/TasksSkeleton.tsx\"\n\ntype Props = {\n  board: DomainBoard\n}\n\nexport const Tasks = ({ board }: Props) => {\n  const { id, filter } = board\n\n  const [page, setPage] = useState(1)\n\n  const { data, isLoading } = useGetBoardTasksQuery({ boardId: id, params: { page } })\n\n  let filteredTasks = data?.data\n  if (filter === \"active\") {\n    filteredTasks = filteredTasks?.filter((task) => task.attributes.status === TaskStatus.New)\n  }\n  if (filter === \"completed\") {\n    filteredTasks = filteredTasks?.filter((task) => task.attributes.status === TaskStatus.Completed)\n  }\n\n  if (isLoading) {\n    return <TasksSkeleton />\n  }\n\n  return (\n    <>\n      {filteredTasks?.length === 0 ? (\n        <p>Тасок нет</p>\n      ) : (\n        <>\n          <List>{filteredTasks?.map((task) => <TaskItem key={task.id} task={task} board={board} page={page} />)}</List>\n          <TasksPagination totalCount={data?.meta.totalCount || 0} page={page} setPage={setPage} />\n        </>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/tasks/ui/Tasks/TasksPagination/TasksPagination.module.css",
    "content": ".pagination {\n  margin-bottom: 10px;\n  display: flex;\n  justify-content: center;\n}\n\n.totalCount {\n  display: flex;\n  justify-content: right;\n  margin-right: 16px;\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/tasks/ui/Tasks/TasksPagination/TasksPagination.tsx",
    "content": "import Pagination from \"@mui/material/Pagination\"\nimport Typography from \"@mui/material/Typography\"\nimport { ChangeEvent } from \"react\"\n\nimport { PAGE_SIZE } from \"@/common/constants\"\n\nimport styles from \"./TasksPagination.module.css\"\n\ntype Props = {\n  totalCount: number\n  page: number\n  setPage: (page: number) => void\n}\n\nexport const TasksPagination = ({ totalCount, page, setPage }: Props) => {\n  const changePage = (_: ChangeEvent<unknown>, page: number) => {\n    setPage(page)\n  }\n\n  return (\n    <>\n      <Pagination\n        count={Math.ceil(totalCount / PAGE_SIZE)}\n        page={page}\n        onChange={changePage}\n        shape=\"rounded\"\n        color=\"primary\"\n        className={styles.pagination}\n      />\n      <div className={styles.totalCount}>\n        <Typography variant=\"caption\">Total: {totalCount}</Typography>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/features/tasks/ui/Tasks/TasksSkeleton/TasksSkeleton.tsx",
    "content": "import Box from \"@mui/material/Box\"\nimport Skeleton from \"@mui/material/Skeleton\"\n\nimport { containerSx } from \"@/common/styles\"\n\nexport const TasksSkeleton = () => (\n  <Box style={{ padding: \"8px 0\" }}>\n    {Array(4)\n      .fill(null)\n      .map((_, id) => (\n        <Box key={id} sx={containerSx}>\n          <Box sx={containerSx} style={{ gap: \"15px\" }}>\n            <Skeleton width={20} height={40} />\n            <Skeleton width={150} height={40} />\n          </Box>\n          <Skeleton width={20} height={40} />\n        </Box>\n      ))}\n  </Box>\n)\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/index.css",
    "content": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/main.tsx",
    "content": "import \"./index.css\"\n\nimport { createRoot } from \"react-dom/client\"\nimport { Provider } from \"react-redux\"\nimport { BrowserRouter } from \"react-router\"\n\nimport { App } from \"@/app/ui/App.tsx\"\n\nimport { store } from \"./app/model/store.ts\"\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <BrowserRouter>\n    <Provider store={store}>\n      <App />\n    </Provider>\n  </BrowserRouter>,\n)\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"Bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"Bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "experiment-apps/trelly-rtk/vite.config.ts",
    "content": "import react from \"@vitejs/plugin-react-swc\"\nimport path from \"path\"\nimport { defineConfig } from \"vite\"\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  resolve: {\n    alias: {\n      \"@/\": `${path.resolve(__dirname, \"src\")}/`,\n    },\n  },\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"musicfun-react-all-stacks\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"private\": true,\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"packageManager\": \"pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"prepare\": \"husky install\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format\": \"prettier --write .\",\n    \"format:check\": \"prettier --check .\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3\",\n    \"@eslint/js\": \"^9.39.1\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-config-next\": \"15.3.3\",\n    \"eslint-config-prettier\": \"^10.1.5\",\n    \"eslint-plugin-prettier\": \"^5.4.1\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"eslint-plugin-react-refresh\": \"^0.4.24\",\n    \"eslint-plugin-simple-import-sort\": \"^12.1.1\",\n    \"eslint-plugin-storybook\": \"9.0.8\",\n    \"globals\": \"^16.5.0\",\n    \"husky\": \"9.1.7\",\n    \"lint-staged\": \"16.1.2\",\n    \"prettier\": \"3.5.3\",\n    \"typescript-eslint\": \"^8.46.4\"\n  },\n  \"lint-staged\": {\n    \"**/*.{js,jsx,ts,tsx}\": [\n      \"prettier --write\"\n    ],\n    \"**/*.{json,css,scss,md}\": [\n      \"prettier --write\"\n    ]\n  },\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/package.json",
    "content": "{\n  \"name\": \"@it-incubator/musicfun-api-sdk\",\n  \"version\": \"1.0.1\",\n  \"description\": \"\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"clean\": \"rm -rf dist\",\n    \"build\": \"tsc\",\n    \"build-and-clean\": \"pnpm run clean && tsc\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"packageManager\": \"pnpm@10.12.1\",\n  \"devDependencies\": {\n    \"typescript\": \"^5.8.3\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.9.0\"\n  }\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/artists/artistsApi.ts",
    "content": "import { artistsEndpoint } from '../../common/apiEntities/apiEntities'\nimport { getApiClient } from '../../v2/request'\nimport type { Artist } from './artistsApi.types.ts'\n\nexport const artistsApi = {\n  findArtists: (name: string) => {\n    return getApiClient().get<Artist[]>(`${artistsEndpoint}/search?term=${name}`)\n  },\n  createArtist: (name: string) => {\n    return getApiClient().post<Artist>(artistsEndpoint, { name })\n  },\n  removeArtist: (id: string) => {\n    return getApiClient().delete<void>(`${artistsEndpoint}/${id}`)\n  },\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/artists/artistsApi.types.ts",
    "content": "export type Artist = {\n  id: string\n  name: string\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/auth/authApi.ts",
    "content": "import { authEndpoint } from '../../common/apiEntities/apiEntities'\nimport { joinUrl } from '../../common/utils/urlHelper'\nimport { getApiClient } from '../../v2/request'\nimport { AuthTokensResponse, MeResponseResponse, RefreshTokensRequest } from './authApi.types'\n\nexport const authApi = {\n  login: (payload: OAuthLoginRequest) => {\n    return getApiClient().post<AuthTokensResponse>(`${authEndpoint}/login`, payload)\n  },\n  logout: (payload: RefreshTokensRequest) => {\n    return getApiClient().post(`${authEndpoint}/logout`, payload)\n  },\n  oauthUrl: (redirectUrl: string): string => {\n    const url = joinUrl(\n      getApiClient().getConfig().baseURL,\n      `/auth/oauth-redirect?callbackUrl=${encodeURIComponent(redirectUrl)}`\n    )\n    return url\n  },\n  refreshToken: (payload: RefreshTokensRequest) => {\n    return getApiClient().post<AuthTokensResponse>(`${authEndpoint}/refresh`, payload)\n  },\n  getMe: () => {\n    return getApiClient().get<MeResponseResponse>(`${authEndpoint}/me`)\n  },\n}\n\nexport type OAuthLoginRequest = {\n  code: string\n  redirectUri: string\n  accessTokenTTL: string // e.g. \"3m\"\n  rememberMe: boolean\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/auth/authApi.types.ts",
    "content": "export type MeResponseResponse = {\n  userId: string\n  login: string\n}\n\nexport type AuthTokensResponse = {\n  refreshToken: string\n  accessToken: string\n}\n\nexport type RefreshTokensRequest = {\n  refreshToken: string\n}\n\nexport const localStorageKeys = {\n  refreshToken: 'musicfun-refresh-token',\n  accessToken: 'musicfun-access-token',\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/playlists/playlistsApi.ts",
    "content": "import { playlistsEndpoint } from '../../common/apiEntities/apiEntities'\nimport { Nullable } from '../../common/types/common.types'\nimport { Images } from '../../common/types/playlists-tracks.types'\nimport { getApiClient } from '../../v2/request'\nimport type {\n  CreatePlaylistArgs,\n  Playlist,\n  PlaylistsResponse,\n  UpdatePlaylistArgs,\n} from './playlistsApi.types.ts'\n\nexport const playlistsApi = {\n  fetchPlaylists: (params: { pageSize?: number; pageNumber: number; search: string }) => {\n    return getApiClient().get<PlaylistsResponse>(playlistsEndpoint, { params })\n  },\n  fetchMyPlaylists: () => {\n    return getApiClient().get<Omit<PlaylistsResponse, 'meta'>>(`${playlistsEndpoint}/my`)\n  },\n  createPlaylist: (args: CreatePlaylistArgs) => {\n    return getApiClient().post<{ data: Playlist }>(playlistsEndpoint, args)\n  },\n  updatePlaylist: (args: { playlistId: string; payload: UpdatePlaylistArgs }) => {\n    const { playlistId, payload } = args\n    return getApiClient().put<void>(`${playlistsEndpoint}/${playlistId}`, payload)\n  },\n  removePlaylist: (playlistId: string) => {\n    return getApiClient().delete<void>(`${playlistsEndpoint}/${playlistId}`)\n  },\n  uploadPlaylistCover: (args: { playlistId: string; file: File }) => {\n    const { playlistId, file } = args\n    const formData = new FormData()\n    formData.append('file', file)\n    return getApiClient().post<Images>(`${playlistsEndpoint}/${playlistId}/images/main`, formData)\n  },\n  fetchPlaylistById: (playlistId: string) => {\n    return getApiClient().get<{ data: Playlist }>(`${playlistsEndpoint}/${playlistId}`)\n  },\n  reorderPlaylist: ({\n    playlistId,\n    putAfterItemId,\n  }: {\n    playlistId: string\n    putAfterItemId: Nullable<string>\n  }) => {\n    return getApiClient().put<void>(`${playlistsEndpoint}/${playlistId}/reorder`, {\n      putAfterItemId,\n    })\n  },\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/playlists/playlistsApi.types.ts",
    "content": "import { Meta } from '../../common/types/common.types'\nimport { Images, User } from '../../common/types/playlists-tracks.types'\n\nexport type Playlist = {\n  id: string\n  type: 'playlists'\n  attributes: PlaylistAttributes\n}\n\nexport type PlaylistAttributes = {\n  title: string\n  description: string\n  addedAt: string\n  updatedAt: string\n  order: number\n  tags: string[]\n  images: Images\n  user: User\n}\n\n// Response\nexport type PlaylistsResponse = {\n  data: Playlist[]\n  meta: Meta\n}\n\n// Arguments\nexport type CreatePlaylistArgs = Pick<PlaylistAttributes, 'title' | 'description'>\n\nexport type UpdatePlaylistArgs = Partial<Pick<PlaylistAttributes, 'title' | 'description' | 'tags'>>\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/tags/tagsApi.ts",
    "content": "import { tagsEndpoint } from '../../common/apiEntities/apiEntities'\nimport { getApiClient } from '../../v2/request'\nimport type { Tag } from './tagsApi.types.ts'\n\nexport const tagsApi = {\n  findTags: (value: string) => {\n    return getApiClient().get<Tag[]>(`${tagsEndpoint}/search?search=${value}`)\n  },\n  createTag: (name: string) => {\n    return getApiClient().post<Tag>(tagsEndpoint, { name })\n  },\n  removeTag: (id: string) => {\n    return getApiClient().delete<void>(`${tagsEndpoint}/${id}`)\n  },\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/tags/tagsApi.types.ts",
    "content": "export type Tag = {\n  id: string\n  name: string\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/tracks/tracksApi.ts",
    "content": "import { playlistsEndpoint, tracksEndpoint } from '../../common/apiEntities/apiEntities.js'\nimport { Nullable } from '../../common/types/common.types'\nimport { Cover } from '../../common/types/playlists-tracks.types.js'\nimport { joinUrl } from '../../common/utils/urlHelper'\nimport { getApiClient, RequestOptions } from '../../v2/request'\nimport type {\n  FetchPlaylistsTracksResponse,\n  FetchTracksArgs,\n  FetchTracksResponse,\n  ReactionResponse,\n  TrackDetailAttributes,\n  TrackDetails,\n  UpdateTrackArgs,\n} from './tracksApi.types.ts'\n\nexport const tracksApi = {\n  fetchTracks: (\n    { pageSize = 3, pageNumber, search = '' }: FetchTracksArgs,\n    opts?: RequestOptions\n  ) => {\n    return getApiClient().get<FetchTracksResponse>(joinUrl(playlistsEndpoint, tracksEndpoint), {\n      ...opts,\n      params: {\n        pageSize,\n        pageNumber,\n        search,\n      },\n    })\n  },\n\n  fetchTracksInPlaylist: ({ playlistId }: { playlistId: string }) => {\n    return getApiClient().get<FetchPlaylistsTracksResponse>(\n      joinUrl(playlistsEndpoint, playlistId, tracksEndpoint)\n    )\n  },\n\n  fetchTrackById: (trackId: string) => {\n    return getApiClient().get<{ data: TrackDetails<TrackDetailAttributes> }>(\n      joinUrl(playlistsEndpoint, tracksEndpoint, trackId)\n    )\n  },\n\n  createTrack: ({ title, file }: { title: string; file: File }) => {\n    const formData = new FormData()\n    formData.append('title', title)\n    formData.append('file', file)\n    return getApiClient().post<{ data: TrackDetails<TrackDetailAttributes> }>(\n      joinUrl(playlistsEndpoint, tracksEndpoint, 'upload'),\n      formData\n    )\n  },\n\n  removeTrack: (trackId: string) => {\n    return getApiClient().delete<void>(joinUrl(playlistsEndpoint, tracksEndpoint, trackId))\n  },\n\n  uploadTrackCover: ({ trackId, file }: { trackId: string; file: File }) => {\n    const formData = new FormData()\n    formData.append('cover', file)\n    return getApiClient().post<Cover>(\n      joinUrl(playlistsEndpoint, tracksEndpoint, trackId, 'cover'),\n      formData\n    )\n  },\n\n  updateTrack: ({ trackId, payload }: { trackId: string; payload: UpdateTrackArgs }) => {\n    return getApiClient().put<{\n      data: TrackDetails<TrackDetailAttributes>\n    }>(joinUrl(playlistsEndpoint, tracksEndpoint, trackId), payload)\n  },\n\n  addTrackToPlaylist: ({ playlistId, trackId }: { playlistId: string; trackId: string }) => {\n    return getApiClient().post<void>(\n      joinUrl(playlistsEndpoint, playlistId, 'relationships', tracksEndpoint),\n      {\n        trackId,\n      }\n    )\n  },\n\n  removeTrackFromPlaylist: ({ playlistId, trackId }: { playlistId: string; trackId: string }) => {\n    return getApiClient().delete<void>(\n      joinUrl(playlistsEndpoint, playlistId, 'relationships', tracksEndpoint, trackId)\n    )\n  },\n\n  reorderTracks: ({\n    trackId,\n    playlistId,\n    putAfterItemId,\n  }: {\n    trackId: string\n    playlistId: string\n    putAfterItemId: Nullable<string>\n  }) => {\n    return getApiClient().put<void>(\n      joinUrl(playlistsEndpoint, playlistId, tracksEndpoint, trackId, 'reorder'),\n      {\n        putAfterItemId,\n      }\n    )\n  },\n\n  like: (trackId: string) => {\n    return getApiClient().post<ReactionResponse>(\n      joinUrl(playlistsEndpoint, tracksEndpoint, trackId, 'like'),\n      {}\n    )\n  },\n\n  dislike: (trackId: string) => {\n    return getApiClient().post<ReactionResponse>(\n      joinUrl(playlistsEndpoint, tracksEndpoint, trackId, 'dislike'),\n      {}\n    )\n  },\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/api/tracks/tracksApi.types.ts",
    "content": "import { Meta, Nullable } from '../../common/types/common.types'\nimport { CurrentUserReaction } from '../../common/types/enums'\nimport { Images, User } from '../../common/types/playlists-tracks.types'\nimport { Artist } from '../artists/artistsApi.types'\nimport { Tag } from '../tags/tagsApi.types'\n\nexport type TrackDetails<T> = {\n  id: string\n  type: 'tracks'\n  attributes: T\n}\n\n// Attributes\nexport type BaseAttributes = {\n  title: string\n  addedAt: string\n  attachments: TrackAttachment[]\n  images: Images\n}\n\nexport type FetchTracksAttributes = BaseAttributes & {\n  user: User\n}\n\nexport type TrackDetailAttributes = BaseAttributes & {\n  lyrics: Nullable<string>\n  releaseDate: Nullable<string>\n  updatedAt: string\n  duration: number\n  processingStatus: TrackProcessingStatus\n  visibility: TrackVisibility\n  tags: Tag[]\n  artists: Artist[]\n  // likes\n  currentUserReaction: CurrentUserReaction\n  dislikesCount: number\n  likesCount: number\n}\n\nexport type PlaylistItemAttributes = BaseAttributes & {\n  updatedAt: string\n  order: number\n}\n\n// Attachment\nexport type TrackAttachment = {\n  id: string\n  addedAt: string\n  updatedAt: string\n  version: number\n  url: string\n  contentType: string\n  originalName: string\n  originalKey: string\n  fileSize: number\n}\n\n// Response\nexport type FetchTracksResponse = {\n  data: TrackDetails<FetchTracksAttributes>[]\n  meta: Meta\n}\n\nexport type FetchPlaylistsTracksResponse = {\n  data: TrackDetails<PlaylistItemAttributes>[]\n  meta: Meta\n}\n\nexport type ReactionResponse = {\n  objectId: string\n  value: number\n  likes: number\n  dislikes: number\n}\n\n// Arguments\nexport type FetchTracksArgs = {\n  pageSize?: number\n  pageNumber: number\n  search?: string\n}\n\nexport type UpdateTrackArgs = {\n  title?: string\n  lyrics?: string\n  visibility?: TrackVisibility\n  releaseDate?: string\n  tagIds?: string[]\n  artistsIds?: string[]\n}\n\n// Literal types\ntype TrackVisibility = 'private' | 'public'\n\ntype TrackProcessingStatus = 'uploaded' | 'converting' | 'ready'\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/common/apiEntities/apiEntities.ts",
    "content": "export const ApiEntities = {\n  tracks: {\n    queryKey: 'tracks',\n    endpoint: 'tracks',\n  },\n  playlists: {\n    queryKey: 'playlists',\n    endpoint: 'playlists',\n  },\n  tags: {\n    queryKey: 'tags',\n    endpoint: 'tags',\n  },\n  artists: {\n    queryKey: 'artists',\n    endpoint: 'artists',\n  },\n  authentication: {\n    queryKey: 'auth',\n    endpoint: 'auth',\n  },\n} as const\n\nexport const tracksEndpoint = ApiEntities.tracks.endpoint\nexport const tracksKey = ApiEntities.tracks.queryKey\n\nexport const playlistsEndpoint = ApiEntities.playlists.endpoint\nexport const playlistsKey = ApiEntities.playlists.queryKey\n\nexport const tagsEndpoint = ApiEntities.tags.endpoint\nexport const tagsKey = ApiEntities.tags.queryKey\n\nexport const artistsEndpoint = ApiEntities.artists.endpoint\nexport const artistsKey = ApiEntities.artists.queryKey\n\nexport const authEndpoint = ApiEntities.authentication.endpoint\nexport const authKey = ApiEntities.authentication.queryKey\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/common/instance/instance.ts",
    "content": "// import axios, { type AxiosError, AxiosInstance } from \"axios\"\n// import { authApi } from \"../../api/auth/authApi\"\n// import { localStorageKeys } from \"../../api/auth/authApi.types\"\n// import { Nullable } from \"../types/common.types\"\n//\n// const config = {\n//   baseURL: null as string | null,\n//   apiKey: null as string | null,\n//   accessTokenLocalStorageKey: localStorageKeys.accessToken,\n//   refreshTokenLocalStorageKey: localStorageKeys.refreshToken\n// }\n//\n// export const setInstanceConfig = (newConfig: Partial<typeof config>) => {\n//   Object.assign(config, newConfig)\n//   createInstance()\n// }\n//\n// function createInstance() {\n//   if (!config.baseURL || !config.apiKey) {\n//     throw new Error(\"call setInstanceConfig to setup api\")\n//   }\n//   const instance = axios.create({\n//     baseURL: config.baseURL!,\n//     headers: {\n//       \"API-KEY\": config.apiKey\n//     }\n//   })\n//\n//   let isRefreshing = false\n//   let failedQueue: {\n//     resolve: (token: string) => void\n//     reject: (err: unknown) => void\n//   }[] = []\n//\n//   const processQueue = (error: unknown, token: Nullable<string>) => {\n//     failedQueue.forEach((prom) => {\n//       if (token) {\n//         prom.resolve(token)\n//       } else {\n//         prom.reject(error)\n//       }\n//     })\n//\n//     failedQueue = []\n//   }\n//\n//\n//   // 👉 REQUEST INTERCEPTOR — добавляем accessToken\n//   instance.interceptors.request.use((config) => {\n//     if (typeof localStorage !== 'undefined') {\n//       const token = localStorage.getItem(localStorageKeys.accessToken)\n//       if (token) {\n//         config.headers.Authorization = `Bearer ${token}`\n//       }\n//     }\n//\n//     if (typeof window === 'undefined') {\n//       config.headers.Origin = \"http://localhost:3000\" // hack for nextjs server request\n//     }\n//\n//     return config\n//   })\n//\n// // 👉 RESPONSE INTERCEPTOR — ловим 401 и пытаемся обновить токен\n//   instance.interceptors.response.use(\n//     (response) => response,\n//     async (error: AxiosError) => {\n//       const originalRequest = error.config as any\n//\n//       if (error.response?.status === 401 && !originalRequest._retry) {\n//         if(typeof localStorage === 'undefined') {\n//           return Promise.reject(error)\n//         }\n//\n//         const refreshToken = localStorage.getItem(localStorageKeys.refreshToken)\n//         if (!refreshToken) {\n//           return Promise.reject(error)\n//         }\n//\n//         if (isRefreshing) {\n//           // если уже идёт refresh — очередь\n//           return new Promise((resolve, reject) => {\n//             failedQueue.push({\n//               resolve: (token: string) => {\n//                 originalRequest.headers.Authorization = `Bearer ${token}`\n//                 resolve(axios(originalRequest))\n//               },\n//               reject\n//             })\n//           })\n//         }\n//\n//         originalRequest._retry = true\n//         isRefreshing = true\n//\n//         try {\n//           const response = await authApi.refreshToken({ refreshToken })\n//           const { accessToken: newToken, refreshToken: newRefresh } = response.data\n//\n//           localStorage.setItem(localStorageKeys.accessToken, newToken)\n//           localStorage.setItem(localStorageKeys.refreshToken, newRefresh)\n//\n//           processQueue(null, newToken)\n//\n//           originalRequest.headers.Authorization = `Bearer ${newToken}`\n//           return axios(originalRequest)\n//         } catch (err) {\n//           processQueue(err, null)\n//           localStorage.removeItem(localStorageKeys.accessToken)\n//           localStorage.removeItem(localStorageKeys.refreshToken)\n//           return Promise.reject(err)\n//         } finally {\n//           isRefreshing = false\n//         }\n//       }\n//\n//       return Promise.reject(error)\n//     }\n//   )\n//   return instance;\n// }\n//\n// let _instance: AxiosInstance | null = null\n//\n// export const getInstance = (): AxiosInstance => {\n//   if (!_instance) {\n//     _instance = createInstance()\n//   }\n//   return _instance!\n// }\n//\n//\n//\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/common/types/common.types.ts",
    "content": "export type Nullable<T> = T | null\n\nexport type Meta = {\n  page: number\n  pageSize: number\n  totalCount: number\n  pagesCount: number\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/common/types/enums.ts",
    "content": "export const CurrentUserReaction = {\n  Like: 1,\n  Dislike: -1,\n  NotResponding: 0,\n} as const\n\nexport type CurrentUserReaction = (typeof CurrentUserReaction)[keyof typeof CurrentUserReaction]\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/common/types/playlists-tracks.types.ts",
    "content": "export type Images = {\n  main: Cover[]\n}\n\nexport type Cover = {\n  type: 'original' | 'medium' | 'thumbnail'\n  width: number\n  height: number\n  fileSize: number\n  url: string\n}\n\nexport type User = {\n  id: string\n  name: string\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/common/utils/urlHelper.ts",
    "content": "export const joinUrl = (...parts: (string | number | undefined | null)[]): string => {\n  return parts\n    .filter(Boolean) // убираем undefined / null / ''\n    .map((p, i) =>\n      i === 0\n        ? String(p).replace(/\\/+$/, '') // у первого убираем хвостовые /\n        : String(p).replace(/^\\/+|\\/+$/g, '')\n    )\n    .join('/')\n}\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/index.ts",
    "content": "export * from './api/artists/artistsApi'\nexport * from './api/artists/artistsApi.types'\nexport * from './api/auth/authApi'\nexport * from './api/auth/authApi.types'\nexport * from './api/playlists/playlistsApi'\nexport * from './api/playlists/playlistsApi.types'\nexport * from './api/tags/tagsApi'\nexport * from './api/tags/tagsApi.types'\nexport * from './api/tracks/tracksApi'\nexport * from './api/tracks/tracksApi.types'\nexport * from './v2/request'\n"
  },
  {
    "path": "packages/musicfun-api-sdk/src/v2/request.ts",
    "content": "// src/common/apiClient.ts\n\nimport { joinUrl } from '../common/utils/urlHelper'\n\nexport interface ApiClientConfig {\n  baseURL: string\n  apiKey?: string\n  getAccessToken: () => string | null\n  getRefreshToken: () => string | null\n  setTokens: (accessToken: string, refreshToken: string) => void\n}\n\nexport interface RequestOptions {\n  params?: Record<string, any>\n  body?: any\n  signal?: AbortSignal\n  nextOptions?: { revalidate?: number; tags?: string[] }\n}\n\ntype RequestInterceptor = (\n  input: RequestInfo,\n  init: RequestInit\n) => Promise<[RequestInfo, RequestInit]>\n\ntype ResponseInterceptor = (response: Response, retry: () => Promise<Response>) => Promise<Response>\n\nexport interface ApiResponse<T> {\n  data: T\n  response: Response\n}\n\nexport class ApiClient {\n  private config: ApiClientConfig\n  private requestInterceptors: RequestInterceptor[] = []\n  private responseInterceptors: ResponseInterceptor[] = []\n  private isRefreshing = false\n  private refreshPromise: Promise<void> | null = null\n\n  constructor(config: ApiClientConfig) {\n    this.config = config\n    this.addRequestInterceptor(this.authRequestInterceptor.bind(this))\n    this.addResponseInterceptor(this.tokenRefreshInterceptor.bind(this))\n  }\n\n  /** Shallow copy of config */\n  getConfig(): ApiClientConfig {\n    return { ...this.config }\n  }\n\n  addRequestInterceptor(fn: RequestInterceptor) {\n    this.requestInterceptors.push(fn)\n  }\n\n  addResponseInterceptor(fn: ResponseInterceptor) {\n    this.responseInterceptors.push(fn)\n  }\n\n  private async authRequestInterceptor(\n    input: RequestInfo,\n    init: RequestInit\n  ): Promise<[RequestInfo, RequestInit]> {\n    const token = this.config.getAccessToken()\n    if (token) init.headers = { ...(init.headers ?? {}), Authorization: `Bearer ${token}` }\n    if (this.config.apiKey)\n      init.headers = { ...(init.headers ?? {}), 'API-KEY': this.config.apiKey }\n    return [input, init]\n  }\n\n  private async tokenRefreshInterceptor(\n    response: Response,\n    retry: () => Promise<Response>\n  ): Promise<Response> {\n    if (response.status !== 401) return response\n    if (!this.refreshPromise) this.refreshPromise = this.handleRefresh()\n    await this.refreshPromise\n    this.refreshPromise = null\n    return retry()\n  }\n\n  private async handleRefresh(): Promise<void> {\n    if (this.isRefreshing) return\n    this.isRefreshing = true\n    try {\n      const refreshToken = this.config.getRefreshToken()\n      if (!refreshToken) throw new Error('No refresh token')\n\n      const url = `${this.config.baseURL}/auth/refresh`\n      const headers: Record<string, string> = { 'Content-Type': 'application/json' }\n      if (this.config.apiKey) headers['API-KEY'] = this.config.apiKey\n\n      const res = await fetch(url, {\n        method: 'POST',\n        headers,\n        body: JSON.stringify({ refreshToken }),\n      })\n      if (!res.ok) throw new Error('Refresh failed')\n\n      const { accessToken, refreshToken: newRefresh } = await res.json()\n      this.config.setTokens(accessToken, newRefresh)\n    } finally {\n      this.isRefreshing = false\n    }\n  }\n\n  private buildUrl(path: string, params?: Record<string, any>): string {\n    const url = new URL(joinUrl(this.config.baseURL, path))\n    if (params) {\n      Object.entries(params).forEach(([k, v]) => {\n        if (v != null) url.searchParams.set(k, String(v))\n      })\n    }\n    return url.toString()\n  }\n\n  private async sendRequest(\n    method: string,\n    path: string,\n    opts: RequestOptions = {}\n  ): Promise<Response> {\n    const baseInput: RequestInfo = this.buildUrl(path, opts.params)\n\n    const headers: Record<string, string> = {}\n    if (!(opts.body instanceof FormData)) {\n      headers['Content-Type'] = 'application/json'\n    }\n\n    const baseInit: RequestInit = {\n      method,\n      headers: headers,\n      body:\n        opts.body instanceof FormData\n          ? opts.body\n          : opts.body\n            ? JSON.stringify(opts.body)\n            : undefined,\n      signal: opts.signal,\n      ...(opts.nextOptions ? { next: opts.nextOptions } : {}),\n    }\n    const fetchCall = async (): Promise<Response> => {\n      // Явно указываем типы, чтобы TS не сузил reqInput до string\n      let reqInput: RequestInfo = baseInput\n      let reqInit: RequestInit = { ...baseInit }\n\n      for (const interceptor of this.requestInterceptors) {\n        const [nextInput, nextInit]: [RequestInfo, RequestInit] = await interceptor(\n          reqInput,\n          reqInit\n        )\n\n        reqInput = nextInput\n        reqInit = nextInit\n      }\n\n      return fetch(reqInput, reqInit)\n    }\n\n    let response = await fetchCall()\n    for (const interceptor of this.responseInterceptors) {\n      response = await interceptor(response, fetchCall)\n    }\n    return response\n  }\n\n  private async request<T>(\n    method: string,\n    path: string,\n    opts?: RequestOptions\n  ): Promise<ApiResponse<T>> {\n    const response = await this.sendRequest(method, path, opts)\n\n    if (!response.ok) {\n      const errText = await response.text()\n      throw new Error(errText || response.statusText)\n    }\n\n    // Клонируем, чтобы не «съесть» оригинальный response\n    const clone = response.clone()\n    const text = await clone.text()\n\n    const data: T | null = text ? (JSON.parse(text) as T) : null\n\n    return { data: data as T, response }\n  }\n\n  /** GET returning `{ data, response }` (data may be null on 204) */\n  get<T>(path: string, opts?: RequestOptions): Promise<ApiResponse<T>> {\n    return this.request<T>('GET', path, opts)\n  }\n\n  /** POST returning `{ data, response }` */\n  post<T, B = any>(path: string, body: B, opts?: RequestOptions): Promise<ApiResponse<T>> {\n    return this.request<T>('POST', path, { ...opts, body })\n  }\n\n  /** PUT returning `{ data, response }` */\n  put<T, B = any>(path: string, body: B, opts?: RequestOptions): Promise<ApiResponse<T>> {\n    return this.request<T>('PUT', path, { ...opts, body })\n  }\n\n  /** DELETE returning `{ data, response }` */\n  delete<T>(path: string, opts?: RequestOptions): Promise<ApiResponse<T>> {\n    return this.request<T>('DELETE', path, opts)\n  }\n}\n\n// singleton instance\nlet client: ApiClient\n\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n  client = new ApiClient(config)\n  return client\n}\n\nexport function getApiClient(): ApiClient {\n  if (!client) throw new Error('ApiClient not initialized')\n  return client\n}\n\n// alias for createApiClient\nexport const configureApi = createApiClient\n"
  },
  {
    "path": "packages/musicfun-api-sdk/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"outDir\": \"dist\",\n    \"module\": \"ESNext\",\n    \"target\": \"ES2019\",\n    \"moduleResolution\": \"Node\",\n    \"strict\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "public/404.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Redirecting...</title>\n    <script>\n      // SPA redirect for GitHub Pages\n      ;(function () {\n        var path = window.location.pathname\n        var search = window.location.search\n        var hash = window.location.hash\n\n        // Determine which app the path belongs to\n        if (path.startsWith('/tanstackquery/') || path === '/tanstackquery') {\n          var fullPath = path + search + hash\n          window.location.replace('/tanstackquery/?spa_redirect=' + encodeURIComponent(fullPath))\n        } else if (path.startsWith('/rtkquery/') || path === '/rtkquery') {\n          var fullPath = path + search + hash\n          window.location.replace('/rtkquery/?spa_redirect=' + encodeURIComponent(fullPath))\n        } else if (path.startsWith('/reatom/') || path === '/reatom') {\n          var fullPath = path + search + hash\n          window.location.replace('/reatom/?spa_redirect=' + encodeURIComponent(fullPath))\n        } else if (path.startsWith('/effector/') || path === '/effector') {\n          var fullPath = path + search + hash\n          window.location.replace('/effector/?spa_redirect=' + encodeURIComponent(fullPath))\n        } else {\n          // Unknown path, redirect to home\n          window.location.replace('/')\n        }\n      })()\n    </script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "public/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>MusicFun Apps - State Management Showcase</title>\n    <style>\n      * {\n        margin: 0;\n        padding: 0;\n        box-sizing: border-box;\n      }\n\n      body {\n        font-family:\n          'Inter',\n          -apple-system,\n          BlinkMacSystemFont,\n          'Segoe UI',\n          'Roboto',\n          sans-serif;\n        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n        min-height: 100vh;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 20px;\n      }\n\n      .container {\n        max-width: 900px;\n        width: 100%;\n      }\n\n      .header {\n        text-align: center;\n        margin-bottom: 60px;\n        color: white;\n      }\n\n      .header h1 {\n        font-size: 3rem;\n        font-weight: 800;\n        margin-bottom: 10px;\n        text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);\n      }\n\n      .header p {\n        font-size: 1.2rem;\n        opacity: 0.95;\n        font-weight: 300;\n      }\n\n      .apps-grid {\n        display: grid;\n        grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\n        gap: 24px;\n        margin-bottom: 40px;\n      }\n\n      .app-card {\n        background: white;\n        border-radius: 16px;\n        padding: 32px;\n        text-decoration: none;\n        color: inherit;\n        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n        box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);\n        position: relative;\n        overflow: hidden;\n      }\n\n      .app-card:hover {\n        transform: translateY(-8px);\n        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);\n      }\n\n      .app-card::before {\n        content: '';\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        height: 4px;\n        background: linear-gradient(90deg, var(--accent-color), var(--accent-color-light));\n      }\n\n      .app-card.tanstack {\n        --accent-color: #ef4444;\n        --accent-color-light: #f87171;\n      }\n\n      .app-card.rtk {\n        --accent-color: #764abc;\n        --accent-color-light: #9575cd;\n      }\n\n      .app-card.reatom {\n        --accent-color: #06b6d4;\n        --accent-color-light: #22d3ee;\n      }\n\n      /* Effector card styles */\n      .app-card.effector {\n        --accent-color: #10b981;\n        --accent-color-light: #34d399;\n      }\n\n      .app-icon {\n        width: 64px;\n        height: 64px;\n        margin-bottom: 20px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        background: linear-gradient(135deg, var(--accent-color), var(--accent-color-light));\n        border-radius: 12px;\n        color: white;\n        font-size: 32px;\n        font-weight: 700;\n      }\n\n      .app-title {\n        font-size: 1.5rem;\n        font-weight: 700;\n        margin-bottom: 8px;\n        color: #1f2937;\n      }\n\n      .app-subtitle {\n        font-size: 0.875rem;\n        color: #6b7280;\n        margin-bottom: 16px;\n        font-weight: 500;\n        text-transform: uppercase;\n        letter-spacing: 0.5px;\n      }\n\n      .app-description {\n        font-size: 1rem;\n        color: #4b5563;\n        line-height: 1.6;\n      }\n\n      .footer {\n        text-align: center;\n        color: white;\n        opacity: 0.9;\n        font-size: 0.9rem;\n      }\n\n      .footer a {\n        color: white;\n        text-decoration: underline;\n        font-weight: 500;\n      }\n\n      @media (max-width: 768px) {\n        .header h1 {\n          font-size: 2rem;\n        }\n\n        .header p {\n          font-size: 1rem;\n        }\n\n        .apps-grid {\n          grid-template-columns: 1fr;\n        }\n      }\n\n      /* Animated gradient background */\n      @keyframes gradient {\n        0% {\n          background-position: 0% 50%;\n        }\n        50% {\n          background-position: 100% 50%;\n        }\n        100% {\n          background-position: 0% 50%;\n        }\n      }\n\n      body {\n        background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);\n        background-size: 200% 200%;\n        animation: gradient 15s ease infinite;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"container\">\n      <div class=\"header\">\n        <h1>🎵 MusicFun Apps</h1>\n        <p>State Management Showcase</p>\n      </div>\n\n      <div class=\"apps-grid\">\n        <a href=\"/tanstackquery/\" class=\"app-card tanstack\">\n          <div class=\"app-icon\">TQ</div>\n          <h2 class=\"app-title\">TanStack Query</h2>\n          <p class=\"app-subtitle\">Server State Management</p>\n          <p class=\"app-description\">\n            Modern data fetching and caching with powerful hooks and automatic refetching.\n          </p>\n        </a>\n\n        <a href=\"/rtkquery/\" class=\"app-card rtk\">\n          <div class=\"app-icon\">RQ</div>\n          <h2 class=\"app-title\">RTK Query</h2>\n          <p class=\"app-subtitle\">Redux Toolkit Query</p>\n          <p class=\"app-description\">\n            Integrated Redux data fetching with automatic cache management and TypeScript support.\n          </p>\n        </a>\n\n        <a href=\"/reatom/\" class=\"app-card reatom\">\n          <div class=\"app-icon\">RA</div>\n          <h2 class=\"app-title\">Reatom</h2>\n          <p class=\"app-subtitle\">Atomic State Management</p>\n          <p class=\"app-description\">\n            Declarative and reactive state management with fine-grained reactivity.\n          </p>\n        </a>\n\n        <a href=\"/effector/\" class=\"app-card effector\">\n          <div class=\"app-icon\">EF</div>\n          <h2 class=\"app-title\">Effector</h2>\n          <p class=\"app-subtitle\">Reactive State Manager</p>\n          <p class=\"app-description\">\n            Predictable and fast state manager with events, stores and effects — great for complex\n            apps.\n          </p>\n        </a>\n      </div>\n\n      <div class=\"footer\">\n        <p>Built with ❤️ using React + Vite | Deployed on GitHub Pages</p>\n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "type-comparison-examples.md",
    "content": "# Type Comparison Examples\n\n## RTK-Query API Track Example\n\nFrom `rtk-query`: `export type ApiTrack = TrackDetails<FetchTracksAttributes>`\nBaseAttributes + FetchTracksAttributes specific fields\n\n```typescript\nconst rtkQueryApiTrack = {\n  id: '1',\n  type: 'tracks' as const,\n  attributes: {\n    // BaseAttributes fields\n    title: 'Days That Matter',\n    addedAt: '2025-06-01T12:00:00Z',\n    attachments: [\n      {\n        id: 'att1',\n        addedAt: '2025-06-01T12:00:00Z',\n        updatedAt: '2025-06-01T12:00:00Z',\n        version: 1,\n        url: 'https://example.com/audio.mp3',\n        contentType: 'audio/mpeg',\n        originalName: 'track.mp3',\n        originalKey: 'uploads/track.mp3',\n        fileSize: 3487234,\n      },\n    ],\n    images: {\n      main: [\n        {\n          type: 'original' as const,\n          width: 100,\n          height: 100,\n          fileSize: 0,\n          url: 'https://unsplash.it/110/110',\n        },\n      ],\n    },\n    currentUserReaction: 0, // 0 - none, 1 - like, -1 - dislike\n    dislikesCount: 2,\n    likesCount: 104,\n\n    // FetchTracksAttributes specific\n    user: {\n      id: '1',\n      name: 'John Doe',\n    },\n  },\n  relationships: {\n    artists: {\n      data: [\n        {\n          id: '1',\n          type: 'artists' as const,\n        },\n      ],\n    },\n  },\n}\n```\n\n---\n\n## Tanstack-Query-Zustand TrackListItemOutput Example\n\n```typescript\nconst tanstackTrackListItemOutput = {\n  id: '1',\n  type: 'tracks',\n  attributes: {\n    title: 'Days That Matter',\n    addedAt: '2025-06-01T12:00:00Z',\n    likesCount: 104,\n    attachments: [\n      {\n        id: 'att1',\n        addedAt: '2025-06-01T12:00:00Z',\n        updatedAt: '2025-06-01T12:00:00Z',\n        version: 1,\n        url: 'https://example.com/audio.mp3',\n        contentType: 'audio/mpeg',\n        originalName: 'track.mp3',\n        fileSize: 3487234,\n      },\n    ],\n    images: {\n      main: [\n        {\n          type: 'original',\n          width: 100,\n          height: 100,\n          fileSize: 0,\n          url: 'https://unsplash.it/110/110',\n        },\n      ],\n    },\n    user: {\n      id: '1',\n      name: 'John Doe',\n    },\n    currentUserReaction: 0, // ReactionValue enum\n    isPublished: true,\n    publishedAt: '2025-06-01T12:00:00Z',\n  },\n  relationships: {\n    artists: {\n      data: [\n        {\n          id: '1',\n          type: 'artists',\n        },\n      ],\n    },\n  },\n}\n```\n\n---\n\n## Tanstack-Query-Zustand TrackDetailsData Example\n\n```typescript\nconst tanstackTrackDetailsData = {\n  id: '1',\n  type: 'tracks',\n  attributes: {\n    title: 'Days That Matter',\n    lyrics: 'Some lyrics text here...',\n    releaseDate: '2025-06-01T12:00:00Z',\n    addedAt: '2025-06-01T12:00:00Z',\n    updatedAt: '2025-06-01T12:00:00Z',\n    duration: 245, // seconds\n    likesCount: 104,\n    dislikesCount: 2,\n    attachments: [\n      {\n        id: 'att1',\n        addedAt: '2025-06-01T12:00:00Z',\n        updatedAt: '2025-06-01T12:00:00Z',\n        version: 1,\n        url: 'https://example.com/audio.mp3',\n        contentType: 'audio/mpeg',\n        originalName: 'track.mp3',\n        fileSize: 3487234,\n      },\n    ],\n    images: {\n      main: [\n        {\n          type: 'original',\n          width: 100,\n          height: 100,\n          fileSize: 0,\n          url: 'https://unsplash.it/110/110',\n        },\n      ],\n    },\n    tags: [\n      {\n        id: 'tag1',\n        name: 'Rock',\n      },\n    ],\n    artists: [\n      {\n        id: '1',\n        name: 'John Doe',\n      },\n    ],\n    user: {\n      id: '1',\n      name: 'John Doe',\n    },\n    isPublished: true,\n    publishedAt: '2025-06-01T12:00:00Z',\n    currentUserReaction: 0, // ReactionValue enum\n  },\n  // NOTE: TrackDetailsData has NO relationships field!\n}\n```\n\n---\n\n## Comparison Analysis\n\n### Key Differences\n\n1. **Relationships Field:**\n\n   - **rtk-query:** Has `relationships.artists` field\n   - **tanstack TrackListItemOutput:** Has `relationships.artists` field\n   - **tanstack TrackDetailsData:** **NO** relationships field at all!\n\n2. **Artist Information:**\n\n   - **rtk-query:** Only relationship ID, needs separate fetch for artist details\n   - **tanstack TrackListItemOutput:** Only relationship ID\n   - **tanstack TrackDetailsData:** Embedded `artists` array with name!\n\n3. **Additional Fields in TrackDetailsData:**\n\n   - `lyrics`: `string | null`\n   - `releaseDate`: `string | null`\n   - `updatedAt`: `string` (vs `addedAt` only in list items)\n   - `duration`: `number` (vs no duration in list items)\n   - `tags`: array of tag objects\n   - `isPublished`: `boolean`\n   - `publishedAt`: `string | null`\n\n4. **Current User Reaction:**\n\n   - **rtk-query:** `number` (0, 1, -1)\n   - **tanstack:** enum type `ReactionValue` (0, 1, -1)\n\n5. **Attachment Structure:**\n\n   - **rtk-query:** Has `originalKey` field\n   - **tanstack:** No `originalKey` field\n\n6. **Enum Values:**\n   - **rtk-query:** String literals (`'tracks'`, `'artists'`)\n   - **tanstack:** Mixed - string literals and enum values\n\n### Conclusion\n\nMy original union type approach was **CORRECT** because:\n\n- `TrackDetailsData` has completely different structure (no relationships)\n- `TrackDetailsData` has embedded artist info instead of relationships\n- Need different handling logic for each format\n"
  },
  {
    "path": "youtube/markup/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "youtube/markup/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:\n\n```js\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n\n      // Remove tseslint.configs.recommended and replace with this\n      ...tseslint.configs.recommendedTypeChecked,\n      // Alternatively, use this for stricter rules\n      ...tseslint.configs.strictTypeChecked,\n      // Optionally, add this for stylistic rules\n      ...tseslint.configs.stylisticTypeChecked,\n\n      // Other configs...\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n\nYou can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:\n\n```js\n// eslint.config.js\nimport reactX from 'eslint-plugin-react-x'\nimport reactDom from 'eslint-plugin-react-dom'\n\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n      // Enable lint rules for React\n      reactX.configs['recommended-typescript'],\n      // Enable lint rules for React DOM\n      reactDom.configs.recommended,\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n"
  },
  {
    "path": "youtube/markup/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport { globalIgnores } from 'eslint/config'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs['recommended-latest'],\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "youtube/markup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Musicfun</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "youtube/markup/package.json",
    "content": "{\n  \"name\": \"musicfun-proj\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19.1.1\",\n    \"react-dom\": \"^19.1.1\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.33.0\",\n    \"@types/react\": \"^19.1.10\",\n    \"@types/react-dom\": \"^19.1.7\",\n    \"@vitejs/plugin-react\": \"^5.0.0\",\n    \"eslint\": \"^9.33.0\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.20\",\n    \"globals\": \"^16.3.0\",\n    \"typescript\": \"~5.8.3\",\n    \"typescript-eslint\": \"^8.39.1\",\n    \"vite\": \"^7.1.2\"\n  }\n}\n"
  },
  {
    "path": "youtube/markup/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "youtube/markup/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "youtube/markup/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "youtube/markup/vite.config.ts",
    "content": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n})\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/.prettierrc",
    "content": "{\n  \"printWidth\": 120,\n  \"semi\": false,\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/AGENTS.md",
    "content": "# Repository Guidelines\n\n## Project Structure & Module Organization\n\nThis project is a Vite + React + TypeScript app.\n\n- `src/app`: app-level setup (`store`, base API/query, app shell and main page).\n- `src/features`: feature slices grouped by domain (`auth`, `playlists`, `tracks`) with `api/`, `model/`, and `ui/`.\n- `src/common`: shared components, hooks, utils, routing, schemas, enums, constants, and types.\n- `src/assets`: static assets (for example `src/assets/images`).\n- `public`: public static files served as-is.\n\nKeep new code feature-first: put domain-specific logic in `src/features/<feature>` and move only reusable pieces to `src/common`.\n\n## Build, Test, and Development Commands\n\nUse `pnpm` (lockfile is `pnpm-lock.yaml`).\n\n- `pnpm dev`: starts local Vite dev server.\n- `pnpm build`: runs TypeScript project build (`tsc -b`) and creates production bundle.\n- `pnpm preview`: serves the production build locally.\n- `pnpm lint`: runs ESLint on the repository.\n\nRun `pnpm lint && pnpm build` before opening a PR.\n\n## Coding Style & Naming Conventions\n\n- Language: TypeScript (`.ts`, `.tsx`), React function components.\n- Formatting: Prettier (`singleQuote: true`, `semi: false`, `printWidth: 120`).\n- Linting: ESLint 9 + `typescript-eslint` + `react-hooks` + `react-refresh`.\n- Naming:\n  - Components and folders: `PascalCase` (for example `PlaylistsPage.tsx`, `Header/`).\n  - Hooks: `useXxx` (for example `useInfiniteScroll.ts`).\n  - Utilities/constants: `camelCase` files (for example `handleErrors.ts`).\n\nPrefer colocating `*.types.ts` and `*.schemas.ts` next to each feature API/model.\n\n## Testing Guidelines\n\nThere is currently no dedicated test runner configured (`test` script is not present). For now:\n\n- Validate changes with `pnpm lint` and `pnpm build`.\n- Manually verify affected flows in `pnpm dev`.\n\nIf you add tests, use Vitest + React Testing Library and place files as `*.test.ts(x)` near the unit under test.\n\n## Commit & Pull Request Guidelines\n\nUse Conventional Commits for every commit message:\n`<type>[optional scope]: <description>`\n\n- Main semantic types:\n  - `fix`: bug fix (PATCH).\n  - `feat`: new feature (MINOR).\n- Allowed additional types: `build`, `chore`, `ci`, `docs`, `style`, `refactor`, `perf`, `test`.\n- Scope is optional and placed in parentheses, for example: `feat(auth): add OAuth callback handler`.\n- Breaking changes:\n  - Add `!` after type/scope, for example `feat(api)!: change token format`, or\n  - Add footer `BREAKING CHANGE: <description>`.\n- Optional body/footer sections are allowed for context and git-trailer-style metadata.\n- Keep commits focused and atomic; avoid mixing unrelated changes.\n- PRs should include: purpose, key changes, manual test steps, and screenshots/GIFs for UI updates.\n- Link related issue/PR numbers when available.\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/README.md",
    "content": "# Musicfun RTK query\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport { globalIgnores } from 'eslint/config'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs['recommended-latest'],\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite + React + TS</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/package.json",
    "content": "{\n  \"name\": \"musicfun-rtk\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@hookform/resolvers\": \"5.2.1\",\n    \"@reduxjs/toolkit\": \"2.8.2\",\n    \"async-mutex\": \"0.5.0\",\n    \"react\": \"19.1.1\",\n    \"react-dom\": \"19.1.1\",\n    \"react-hook-form\": \"7.61.1\",\n    \"react-redux\": \"9.2.0\",\n    \"react-router\": \"7.7.1\",\n    \"react-toastify\": \"11.0.5\",\n    \"socket.io-client\": \"4.8.1\",\n    \"zod\": \"4.0.17\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"9.32.0\",\n    \"@types/node\": \"24.1.0\",\n    \"@types/react\": \"19.1.9\",\n    \"@types/react-dom\": \"19.1.7\",\n    \"@vitejs/plugin-react-swc\": \"3.11.0\",\n    \"eslint\": \"9.32.0\",\n    \"eslint-plugin-react-hooks\": \"5.2.0\",\n    \"eslint-plugin-react-refresh\": \"0.4.20\",\n    \"globals\": \"16.3.0\",\n    \"prettier\": \"3.6.2\",\n    \"typescript\": \"5.8.3\",\n    \"typescript-eslint\": \"8.38.0\",\n    \"vite\": \"7.0.6\"\n  }\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/app/api/baseApi.ts",
    "content": "import { createApi } from '@reduxjs/toolkit/query/react'\n\nimport { baseQueryWithReauth } from '@/app/api/baseQueryWithReauth.ts'\n\nexport const baseApi = createApi({\n  reducerPath: 'baseApi',\n  tagTypes: ['Playlist', 'Auth'],\n  baseQuery: baseQueryWithReauth,\n  endpoints: () => ({}),\n  // skipSchemaValidation: process.env.NODE_ENV === 'production',\n})\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/app/api/baseQuery.ts",
    "content": "import { fetchBaseQuery } from '@reduxjs/toolkit/query/react'\n\nimport { AUTH_KEYS } from '@/common/constants'\n\nexport const baseQuery = fetchBaseQuery({\n  baseUrl: import.meta.env.VITE_BASE_URL,\n  headers: {\n    'API-KEY': import.meta.env.VITE_API_KEY,\n  },\n  prepareHeaders: (headers) => {\n    const accessToken = localStorage.getItem(AUTH_KEYS.accessToken)\n    if (accessToken) {\n      headers.set('Authorization', `Bearer ${accessToken}`)\n    }\n    return headers\n  },\n})\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/app/api/baseQueryWithReauth.ts",
    "content": "import type { BaseQueryFn, FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query'\nimport { Mutex } from 'async-mutex'\n\nimport { baseApi } from '@/app/api/baseApi.ts'\nimport { baseQuery } from '@/app/api/baseQuery.ts'\nimport { AUTH_KEYS } from '@/common/constants'\nimport { handleErrors, isTokens } from '@/common/utils'\n\n// create a new mutex\nconst mutex = new Mutex()\n\nexport const baseQueryWithReauth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (\n  args,\n  api,\n  extraOptions,\n) => {\n  // await new Promise((resolve) => setTimeout(resolve, 2000)) // delay\n\n  // wait until the mutex is available without locking it\n  await mutex.waitForUnlock()\n\n  let result = await baseQuery(args, api, extraOptions)\n\n  if (result.error && result.error.status === 401) {\n    // checking whether the mutex is locked\n    if (!mutex.isLocked()) {\n      const release = await mutex.acquire()\n      try {\n        const refreshToken = localStorage.getItem(AUTH_KEYS.refreshToken)\n\n        const refreshResult = await baseQuery(\n          {\n            url: '/auth/refresh',\n            method: 'post',\n            body: { refreshToken },\n          },\n          api,\n          extraOptions,\n        )\n\n        if (refreshResult.data && isTokens(refreshResult.data)) {\n          localStorage.setItem(AUTH_KEYS.accessToken, refreshResult.data.accessToken)\n          localStorage.setItem(AUTH_KEYS.refreshToken, refreshResult.data.refreshToken)\n          // retry the initial query\n          result = await baseQuery(args, api, extraOptions)\n        } else {\n          // @ts-expect-error\n          api.dispatch(baseApi.endpoints.logout.initiate())\n        }\n      } finally {\n        // release must be called once the mutex should be released again.\n        release()\n      }\n    } else {\n      // wait until the mutex is available without locking it\n      await mutex.waitForUnlock()\n      result = await baseQuery(args, api, extraOptions)\n    }\n  }\n\n  if (result.error && result.error.status !== 401) {\n    handleErrors(result.error)\n  }\n\n  return result\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/app/model/store.ts",
    "content": "import { configureStore } from '@reduxjs/toolkit'\nimport { setupListeners } from '@reduxjs/toolkit/query'\n\nimport { baseApi } from '@/app/api/baseApi.ts'\n\nexport const store = configureStore({\n  reducer: {\n    [baseApi.reducerPath]: baseApi.reducer,\n  },\n  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(baseApi.middleware),\n})\n\nexport type RootState = ReturnType<typeof store.getState>\n\nsetupListeners(store.dispatch)\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/app/ui/App/App.module.css",
    "content": ".layout {\n  max-width: 1186px;\n  margin: 0 auto 200px;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/app/ui/App/App.tsx",
    "content": "import { ToastContainer } from 'react-toastify'\n\nimport { Header, LinearProgress } from '@/common/components'\nimport { useGlobalLoading } from '@/common/hooks'\nimport { Routing } from '@/common/routing'\n\nimport s from './App.module.css'\n\nexport const App = () => {\n  const isGlobalLoading = useGlobalLoading()\n\n  return (\n    <>\n      <Header />\n      {isGlobalLoading && <LinearProgress />}\n      <div className={s.layout}>\n        <Routing />\n      </div>\n      <ToastContainer />\n    </>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/app/ui/MainPage/MainPage.tsx",
    "content": "import { useGetMeQuery } from '@/features/auth/api/authApi.ts'\n\nexport const MainPage = () => {\n  const { data } = useGetMeQuery()\n\n  return (\n    <div>\n      <h1>Main page</h1>\n      <div>login: {data?.login} </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/Header/Header.module.css",
    "content": ".container {\n  border-bottom: 1px solid black;\n  padding: 0 100px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.list {\n  display: flex;\n  gap: 40px;\n}\n\n.activeLink {\n  font-weight: bold;\n}\n\n.loginContainer {\n  display: flex;\n  gap: 10px;\n  align-items: center;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/Header/Header.tsx",
    "content": "import { Link, NavLink } from 'react-router'\n\nimport { Path } from '@/common/routing'\nimport { useGetMeQuery, useLogoutMutation } from '@/features/auth/api/authApi.ts'\nimport { Login } from '@/features/auth/ui/Login/Login.tsx'\n\nimport s from './Header.module.css'\n\nconst navItems = [\n  { to: Path.Main, label: 'Main' },\n  { to: Path.Playlists, label: 'Playlists' },\n  { to: Path.Tracks, label: 'Tracks' },\n]\n\nexport const Header = () => {\n  const { data } = useGetMeQuery()\n  const [logout] = useLogoutMutation()\n\n  const logoutHandler = () => logout()\n\n  return (\n    <header className={s.container}>\n      <nav>\n        <ul className={s.list}>\n          {navItems.map((item) => (\n            <li key={item.to}>\n              <NavLink to={item.to} className={({ isActive }) => `link ${isActive ? s.activeLink : ''}`}>\n                {item.label}\n              </NavLink>\n            </li>\n          ))}\n        </ul>\n      </nav>\n      {data && (\n        <div className={s.loginContainer}>\n          <Link to={Path.Profile}>{data.login}</Link>\n          <button onClick={logoutHandler}>logout</button>\n        </div>\n      )}\n\n      {!data && <Login />}\n    </header>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/LinearProgress/LinearProgress.module.css",
    "content": ".root {\n  background: #23272f;\n  border-radius: 8px;\n  overflow: hidden;\n  position: relative;\n  width: 100%;\n}\n\n.bar {\n  height: 100%;\n  background: #ddd;\n  position: absolute;\n}\n\n@keyframes indeterminate1 {\n  0% {\n    left: -35%;\n    right: 100%;\n  }\n  60% {\n    left: 100%;\n    right: -90%;\n  }\n  100% {\n    left: 100%;\n    right: -90%;\n  }\n}\n\n@keyframes indeterminate2 {\n  0% {\n    left: -200%;\n    right: 100%;\n  }\n  60% {\n    left: 107%;\n    right: -8%;\n  }\n  100% {\n    left: 107%;\n    right: -8%;\n  }\n}\n\n.indeterminate1 {\n  animation: indeterminate1 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;\n}\n\n.indeterminate2 {\n  animation: indeterminate2 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/LinearProgress/LinearProgress.tsx",
    "content": "import s from './LinearProgress.module.css'\n\ntype Props = {\n  height?: number\n}\n\nexport const LinearProgress = ({ height = 4 }: Props) => {\n  return (\n    <div className={s.root} style={{ height }}>\n      <div className={`${s.bar} ${s.indeterminate1}`} />\n      <div className={`${s.bar} ${s.indeterminate2}`} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/PageNotFound/PageNotFound.module.css",
    "content": ".title {\n  text-align: center;\n  font-size: 250px;\n  margin: 0;\n}\n\n.subtitle {\n  text-align: center;\n  font-size: 50px;\n  margin: 0;\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/PageNotFound/PageNotFound.tsx",
    "content": "import s from './PageNotFound.module.css'\n\nexport const PageNotFound = () => {\n  return (\n    <>\n      <h1 className={s.title}>404</h1>\n      <h2 className={s.subtitle}>page not found</h2>\n    </>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/Pagination/Pagination.module.css",
    "content": ".container {\n  display: flex;\n  align-content: center;\n  align-items: center;\n  margin: 0 auto;\n  gap: 40px;\n}\n\n.pagination {\n  display: flex;\n  gap: 8px;\n  justify-content: center;\n}\n\n.pageButton {\n  padding: 4px 10px;\n  background: white;\n  border: 1px solid #aaa;\n  border-radius: 4px;\n  cursor: pointer;\n}\n\n.pageButtonActive {\n  background: #ececec;\n  cursor: default;\n}\n\n.ellipsis {\n  padding: 4px 10px;\n  color: #888;\n  user-select: none;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/Pagination/Pagination.tsx",
    "content": "import { getPaginationPages } from '@/common/utils'\n\nimport s from './Pagination.module.css'\n\ntype Props = {\n  currentPage: number\n  setCurrentPage: (page: number) => void\n  pagesCount: number\n  pageSize: number\n  changePageSize: (size: number) => void\n}\n\nexport const Pagination = ({ currentPage, setCurrentPage, pagesCount, pageSize, changePageSize }: Props) => {\n  if (pagesCount <= 1) return null\n\n  const pages = getPaginationPages(currentPage, pagesCount)\n\n  return (\n    <div className={s.container}>\n      <div className={s.pagination}>\n        {pages.map((page, idx) =>\n          page === '...' ? (\n            <span className={s.ellipsis} key={`ellipsis-${idx}`}>\n              ...\n            </span>\n          ) : (\n            <button\n              key={page}\n              className={page === currentPage ? `${s.pageButton} ${s.pageButtonActive}` : s.pageButton}\n              onClick={() => page !== currentPage && setCurrentPage(Number(page))}\n              disabled={page === currentPage}\n              type=\"button\"\n            >\n              {page}\n            </button>\n          ),\n        )}\n      </div>\n      <label>\n        Show\n        <select value={pageSize} onChange={(e) => changePageSize(Number(e.target.value))}>\n          {[2, 4, 8, 16, 32].map((size) => (\n            <option value={size} key={size}>\n              {size}\n            </option>\n          ))}\n        </select>\n        per page\n      </label>\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/components/index.tsx",
    "content": "export { Header } from './Header/Header.tsx'\nexport { LinearProgress } from './LinearProgress/LinearProgress.tsx'\nexport { PageNotFound } from './PageNotFound/PageNotFound.tsx'\nexport { Pagination } from './Pagination/Pagination.tsx'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/constants/constants.ts",
    "content": "export const AUTH_KEYS = {\n  accessToken: 'musicfun-access-token',\n  refreshToken: 'musicfun-refresh-token',\n} as const\n\nexport const SOCKET_EVENTS = {\n  TRACK_PUBLISHED: 'tracks.track-published',\n  TRACK_ADDED_TO_PLAYLIST: 'tracks.track-added-to-playlist',\n  TRACK_LIKED: 'tracks.track-liked',\n  TRACK_IMAGE_PROCESSED: 'tracks.track-image-processed',\n  PLAYLIST_IMAGE_PROCESSED: 'tracks.playlist-image-processed',\n  PLAYLIST_CREATED: 'tracks.playlist-created',\n  PLAYLIST_UPDATED: 'tracks.playlist-updated',\n} as const\n\nexport type SocketEvents = (typeof SOCKET_EVENTS)[keyof typeof SOCKET_EVENTS]\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/constants/index.ts",
    "content": "export * from './constants.ts'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/enums/enums.ts",
    "content": "export const CurrentUserReaction = {\n  Like: 1,\n  Dislike: -1,\n  None: 0,\n} as const\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/enums/index.ts",
    "content": "export * from './enums.ts'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/hooks/index.ts",
    "content": "export { useDebounceValue } from './useDebounceValue.ts'\nexport { useGlobalLoading } from './useGlobalLoading.ts'\nexport { useInfiniteScroll } from './useInfiniteScroll.ts'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/hooks/useDebounceValue.ts",
    "content": "import { useEffect, useState } from 'react'\n\nexport const useDebounceValue = <T>(value: T, delay: number = 700): T => {\n  const [debounced, setDebounced] = useState(value)\n\n  useEffect(() => {\n    const handler = setTimeout(() => setDebounced(value), delay)\n    return () => clearTimeout(handler)\n  }, [value, delay])\n\n  return debounced\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/hooks/useGlobalLoading.ts",
    "content": "import { useSelector } from 'react-redux'\n\nimport type { RootState } from '@/app/model/store.ts'\nimport { playlistsApi } from '@/features/playlists/api/playlistsApi.ts'\nimport { tracksApi } from '@/features/tracks/api/tracksApi.ts'\n\n// List of endpoints to exclude from the global indicator\nconst excludedEndpoints = [playlistsApi.endpoints.fetchPlaylists.name, tracksApi.endpoints.fetchTracks.name]\n\nexport const useGlobalLoading = () => {\n  return useSelector((state: RootState) => {\n    // Get all active requests from RTK Query API\n    const queries = Object.values(state.baseApi.queries || {})\n    const mutations = Object.values(state.baseApi.mutations || {})\n\n    const hasActiveQueries = queries.some((query) => {\n      if (query?.status !== 'pending') return\n\n      if (excludedEndpoints.includes(query.endpointName)) {\n        const completedQueries = queries.filter((q) => q?.status === 'fulfilled')\n        return completedQueries.length > 0\n      }\n    })\n\n    const hasActiveMutations = mutations.some((mutation) => mutation?.status === 'pending')\n\n    return hasActiveQueries || hasActiveMutations\n  })\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/hooks/useInfiniteScroll.ts",
    "content": "import { useCallback, useEffect, useRef } from 'react'\n\ntype Props = {\n  hasNextPage: boolean\n  isFetching: boolean\n  fetchNextPage: () => void\n  rootMargin?: string\n  threshold?: number\n}\n\nexport const useInfiniteScroll = ({\n  hasNextPage,\n  isFetching,\n  fetchNextPage,\n  rootMargin = '100px',\n  threshold = 0.1,\n}: Props) => {\n  const observerRef = useRef<HTMLDivElement>(null)\n\n  const loadMoreHandler = useCallback(() => {\n    if (hasNextPage && !isFetching) {\n      fetchNextPage()\n    }\n  }, [hasNextPage, isFetching, fetchNextPage])\n\n  useEffect(() => {\n    // IntersectionObserver monitors elements and reports how visible they are in the viewport\n    // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API\n    const observer = new IntersectionObserver(\n      (entries) => {\n        // entries - observed element\n        if (entries.length > 0 && entries[0].isIntersecting) {\n          loadMoreHandler()\n        }\n      },\n      {\n        root: null, // Tracking relative to the browser window (viewport). null = entire screen\n        rootMargin, // Start loading before the element appears\n        threshold, // Trigger when % of the element becomes visible\n      },\n    )\n\n    const currentObserverRef = observerRef.current\n    if (currentObserverRef) {\n      // starts observing the element\n      observer.observe(currentObserverRef)\n    }\n\n    // Cleanup function - stops observing when component unmounts\n    return () => {\n      if (currentObserverRef) {\n        observer.unobserve(currentObserverRef)\n      }\n    }\n  }, [loadMoreHandler, rootMargin, threshold])\n\n  return { observerRef }\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/routing/Routing.tsx",
    "content": "import { Route, Routes } from 'react-router'\n\nimport { MainPage } from '@/app/ui/MainPage/MainPage.tsx'\nimport { PageNotFound } from '@/common/components'\nimport { OAuthCallback } from '@/features/auth/ui/OAuthCallback/OAuthCallback.tsx'\nimport { ProfilePage } from '@/features/auth/ui/ProfilePage/ProfilePage.tsx'\nimport { PlaylistsPage } from '@/features/playlists/ui/PlaylistsPage.tsx'\nimport { TracksPage } from '@/features/tracks/ui/TracksPage.tsx'\n\nexport const Path = {\n  Main: '/',\n  Playlists: '/playlists',\n  Tracks: '/tracks',\n  Profile: '/profile',\n  OAuthRedirect: '/oauth/callback',\n  NotFound: '*',\n} as const\n\nexport const Routing = () => (\n  <Routes>\n    <Route path={Path.Main} element={<MainPage />} />\n    <Route path={Path.Playlists} element={<PlaylistsPage />} />\n    <Route path={Path.Tracks} element={<TracksPage />} />\n    <Route path={Path.Profile} element={<ProfilePage />} />\n    <Route path={Path.OAuthRedirect} element={<OAuthCallback />} />\n    <Route path={Path.NotFound} element={<PageNotFound />} />\n  </Routes>\n)\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/routing/index.ts",
    "content": "export * from './Routing.tsx'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/schemas/index.ts",
    "content": "export * from './schemas.ts'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/schemas/schemas.ts",
    "content": "import * as z from 'zod'\n\nimport { CurrentUserReaction } from '@/common/enums'\n\nexport const tagSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n})\n\nexport const userSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n})\n\nexport const coverSchema = z.object({\n  type: z.literal(['original', 'medium', 'thumbnail']),\n  width: z.int().positive(),\n  height: z.int().positive(),\n  fileSize: z.int().positive(),\n  url: z.url(),\n})\n\nexport const imagesSchema = z.object({\n  main: z.array(coverSchema),\n})\n\nexport const currentUserReactionSchema = z.enum(CurrentUserReaction)\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/socket/getSocket.ts",
    "content": "import { io, Socket } from 'socket.io-client'\n\nlet socket: Socket | null = null\n\nexport const getSocket = () => {\n  if (!socket) {\n    socket = io(import.meta.env.VITE_SOCKET_URL, {\n      path: '/api/1.0/ws',\n      transports: ['websocket'],\n    })\n\n    socket.on('connect', () => console.log('✅ Connected to server'))\n    socket.on('disconnect', () => console.log('❌ Connection destroyed'))\n  }\n\n  return socket\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/socket/index.ts",
    "content": "export { getSocket } from './getSocket.ts'\nexport { subscribeToEvent } from './subscribeToEvent.ts'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/socket/subscribeToEvent.ts",
    "content": "import type { SocketEvents } from '@/common/constants'\nimport { getSocket } from '@/common/socket/getSocket.ts'\n\nexport const subscribeToEvent = <T>(event: SocketEvents, callback: (data: T) => void) => {\n  const socket = getSocket()\n\n  socket.on(event, callback)\n\n  return () => {\n    socket.off(event, callback)\n  }\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/types/index.ts",
    "content": "export * from './types.ts'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/types/types.ts",
    "content": "import * as z from 'zod'\n\nimport { coverSchema, currentUserReactionSchema, imagesSchema, tagSchema, userSchema } from '@/common/schemas'\n\nexport type Tag = z.infer<typeof tagSchema>\nexport type User = z.infer<typeof userSchema>\nexport type Cover = z.infer<typeof coverSchema>\nexport type Images = z.infer<typeof imagesSchema>\nexport type CurrentUserReaction = z.infer<typeof currentUserReactionSchema>\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/errorToast.ts",
    "content": "import { toast } from 'react-toastify'\n\nexport const errorToast = (message: string, error?: unknown) => {\n  toast(message, { theme: 'colored', type: 'error' })\n\n  if (error) {\n    console.error(`${message}\\n`, error)\n  }\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/getPaginationPages.ts",
    "content": "const SIBLING_COUNT = 1\n\n/**\n * Generates an array of pages for pagination display with ellipses\n */\nexport const getPaginationPages = (currentPage: number, pagesCount: number): (number | '...')[] => {\n  if (pagesCount <= 1) return []\n\n  const pages: (number | '...')[] = []\n\n  // Range boundaries around the current page\n  const leftSibling = Math.max(2, currentPage - SIBLING_COUNT)\n  const rightSibling = Math.min(pagesCount - 1, currentPage + SIBLING_COUNT)\n\n  // Always show the first page\n  pages.push(1)\n\n  // Ellipsis on the left\n  if (leftSibling > 2) {\n    pages.push('...')\n  }\n\n  // Neighboring pages around the current page\n  for (let page = leftSibling; page <= rightSibling; page++) {\n    pages.push(page)\n  }\n\n  // Ellipsis on the right\n  if (rightSibling < pagesCount - 1) {\n    pages.push('...')\n  }\n\n  // Always show the last page (if more than one)\n  if (pagesCount > 1) {\n    pages.push(pagesCount)\n  }\n\n  return pages\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/handleErrors.ts",
    "content": "import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'\n\nimport { errorToast, isErrorWithDetailArray, isErrorWithProperty, trimToMaxLength } from '@/common/utils'\n\nexport const handleErrors = (error: FetchBaseQueryError) => {\n  if (error) {\n    switch (error.status) {\n      case 'FETCH_ERROR':\n      case 'PARSING_ERROR':\n      case 'CUSTOM_ERROR':\n      case 'TIMEOUT_ERROR':\n        errorToast(error.error)\n        break\n\n      case 400:\n        if (isErrorWithDetailArray(error.data)) {\n          const errorMessage = error.data.errors[0].detail\n          if (errorMessage.includes('refresh')) return\n          errorToast(trimToMaxLength(error.data.errors[0].detail))\n        } else {\n          errorToast(JSON.stringify(error.data))\n        }\n        break\n\n      case 403:\n        if (isErrorWithDetailArray(error.data)) {\n          errorToast(trimToMaxLength(error.data.errors[0].detail))\n        } else {\n          errorToast(JSON.stringify(error.data))\n        }\n        break\n\n      case 404:\n        if (isErrorWithProperty(error.data, 'error')) {\n          errorToast(error.data.error)\n        } else {\n          errorToast(JSON.stringify(error.data))\n        }\n        break\n\n      case 429:\n        if (isErrorWithProperty(error.data, 'message')) {\n          errorToast(error.data.message)\n        } else {\n          errorToast(JSON.stringify(error.data))\n        }\n        break\n\n      default:\n        if (error.status >= 500 && error.status < 600) {\n          errorToast('Server error occurred. Please try again later.', error)\n        } else {\n          errorToast('Some error occurred')\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/index.ts",
    "content": "export { errorToast } from './errorToast.ts'\nexport { getPaginationPages } from './getPaginationPages.ts'\nexport { handleErrors } from './handleErrors.ts'\nexport { isErrorWithDetailArray } from './isErrorWithDetailArray.ts'\nexport { isErrorWithProperty } from './isErrorWithProperty.ts'\nexport { isTokens } from './isTokens.ts'\nexport { trimToMaxLength } from './trimToMaxLength.ts'\nexport { withZodCatch } from './withZodCatch.ts'\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/isErrorWithDetailArray.ts",
    "content": "export function isErrorWithDetailArray(error: unknown): error is { errors: { detail: string }[] } {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    'errors' in error &&\n    Array.isArray((error as any).errors) &&\n    (error as any).errors.length > 0 &&\n    typeof (error as any).errors[0].detail === 'string'\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/isErrorWithProperty.ts",
    "content": "export function isErrorWithProperty<T extends string>(error: unknown, property: T): error is Record<T, string> {\n  return (\n    typeof error === 'object' &&\n    error != null &&\n    property in error &&\n    typeof (error as Record<string, unknown>)[property] === 'string'\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/isTokens.ts",
    "content": "export const isTokens = (data: unknown): data is { accessToken: string; refreshToken: string } => {\n  return typeof data === 'object' && data !== null && 'accessToken' in data && 'refreshToken' in data\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/trimToMaxLength.ts",
    "content": "export function trimToMaxLength(str: string, maxLength = 100): string {\n  return str.length > maxLength ? str.slice(0, maxLength - 3) + '...' : str\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/common/utils/withZodCatch.ts",
    "content": "import { type FetchBaseQueryError, NamedSchemaError } from '@reduxjs/toolkit/query/react'\nimport type { ZodType } from 'zod'\n\nimport { errorToast } from '@/common/utils/errorToast.ts'\n\nexport const withZodCatch = <T extends ZodType>(schema: T) => ({\n  responseSchema: schema,\n  catchSchemaFailure: (err: NamedSchemaError): FetchBaseQueryError => {\n    errorToast('Zod error. Details in the console', err.issues)\n    return {\n      status: 'CUSTOM_ERROR',\n      error: 'Schema validation failed',\n    }\n  },\n})\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/auth/api/authApi.ts",
    "content": "import { baseApi } from '@/app/api/baseApi.ts'\nimport { AUTH_KEYS } from '@/common/constants'\nimport { withZodCatch } from '@/common/utils'\nimport type { LoginArgs } from '@/features/auth/api/authApi.types.ts'\nimport { loginResponseSchema, meResponseSchema } from '@/features/auth/model/auth.schemas.ts'\n\nexport const authApi = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    getMe: build.query({\n      query: () => 'auth/me',\n      ...withZodCatch(meResponseSchema),\n      providesTags: ['Auth'],\n    }),\n    login: build.mutation({\n      query: (payload: LoginArgs) => ({\n        method: 'post',\n        url: 'auth/login',\n        body: { ...payload, accessTokenTTL: '15m' },\n      }),\n      onQueryStarted: async (_args, { dispatch, queryFulfilled }) => {\n        const { data } = await queryFulfilled\n        localStorage.setItem(AUTH_KEYS.accessToken, data.accessToken)\n        localStorage.setItem(AUTH_KEYS.refreshToken, data.refreshToken)\n        // Invalidate after saving tokens\n        dispatch(authApi.util.invalidateTags(['Auth']))\n      },\n      ...withZodCatch(loginResponseSchema),\n    }),\n    logout: build.mutation<void, void>({\n      query: () => {\n        const refreshToken = localStorage.getItem(AUTH_KEYS.refreshToken)\n        return { method: 'post', url: 'auth/logout', body: { refreshToken } }\n      },\n      onQueryStarted: async (_args, { dispatch, queryFulfilled }) => {\n        await queryFulfilled\n        localStorage.removeItem(AUTH_KEYS.accessToken)\n        localStorage.removeItem(AUTH_KEYS.refreshToken)\n        dispatch(baseApi.util.resetApiState())\n      },\n    }),\n  }),\n})\n\nexport const { useGetMeQuery, useLoginMutation, useLogoutMutation } = authApi\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/auth/api/authApi.types.ts",
    "content": "import * as z from 'zod'\n\nimport { loginResponseSchema, type meResponseSchema } from '@/features/auth/model/auth.schemas.ts'\n\nexport type MeResponse = z.infer<typeof meResponseSchema>\nexport type LoginResponse = z.infer<typeof loginResponseSchema>\n\n// Arguments\nexport type LoginArgs = {\n  code: string\n  redirectUri: string\n  rememberMe: boolean\n  accessTokenTTL?: string // e.g. \"3m\"\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/auth/model/auth.schemas.ts",
    "content": "import * as z from 'zod'\n\nexport const meResponseSchema = z.object({\n  userId: z.string(),\n  login: z.string(),\n})\n\nexport const loginResponseSchema = z.object({\n  refreshToken: z.jwt(),\n  accessToken: z.jwt(),\n})\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/auth/ui/Login/Login.tsx",
    "content": "import { Path } from '@/common/routing'\nimport { useLoginMutation } from '@/features/auth/api/authApi.ts'\n\nexport const Login = () => {\n  const [login] = useLoginMutation()\n\n  const loginHandler = () => {\n    const redirectUri = import.meta.env.VITE_DOMAIN_ADDRESS + Path.OAuthRedirect\n\n    const url = `${import.meta.env.VITE_BASE_URL}/auth/oauth-redirect?callbackUrl=${redirectUri}`\n\n    window.open(url, 'oauthPopup', 'width=500, height=600')\n\n    const receiveMessage = (event: MessageEvent) => {\n      if (event.origin !== import.meta.env.VITE_DOMAIN_ADDRESS) return\n\n      const { code } = event.data\n      if (!code) return\n\n      window.removeEventListener('message', receiveMessage)\n      login({ code, redirectUri, rememberMe: false })\n    }\n\n    window.addEventListener('message', receiveMessage)\n  }\n\n  return (\n    <button type={'button'} onClick={loginHandler}>\n      login\n    </button>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/auth/ui/OAuthCallback/OAuthCallback.tsx",
    "content": "// Component triggered after successful OAuth authorization,\n// its purpose is to send the code back to the main app window and close the popup\nimport { useEffect } from 'react'\n\nexport const OAuthCallback = () => {\n  useEffect(() => {\n    // Get the current URL\n    const url = new URL(window.location.href)\n\n    // Extract code from query parameters\n    const code = url.searchParams.get('code')\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, '*')\n    }\n\n    window.close()\n  }, [])\n\n  return <p>Logging you in...</p>\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/auth/ui/ProfilePage/ProfilePage.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/auth/ui/ProfilePage/ProfilePage.tsx",
    "content": "import { Navigate } from 'react-router'\n\nimport { Path } from '@/common/routing'\nimport { useGetMeQuery } from '@/features/auth/api/authApi.ts'\nimport { useFetchPlaylistsQuery } from '@/features/playlists/api/playlistsApi.ts'\nimport { CreatePlaylistForm } from '@/features/playlists/ui/CreatePlaylistForm/CreatePlaylistForm.tsx'\nimport { PlaylistsList } from '@/features/playlists/ui/PlaylistsList/PlaylistsList.tsx'\n\nimport s from './ProfilePage.module.css'\n\nexport const ProfilePage = () => {\n  const { data: meResponse, isLoading: isMeLoading } = useGetMeQuery()\n\n  const { data: playlistsResponse, isLoading } = useFetchPlaylistsQuery(\n    { userId: meResponse?.userId },\n    { skip: !meResponse?.userId },\n  )\n\n  if (isLoading || isMeLoading) return <h1>Skeleton loader ...</h1>\n\n  if (!isMeLoading && !meResponse) return <Navigate to={Path.Playlists} />\n\n  return (\n    <div>\n      <h1>{meResponse?.login} page</h1>\n      <div className={s.container}>\n        <CreatePlaylistForm />\n        <PlaylistsList isPlaylistsLoading={isLoading || isMeLoading} playlists={playlistsResponse?.data || []} />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/api/playlistsApi.ts",
    "content": "import { baseApi } from '@/app/api/baseApi.ts'\nimport { SOCKET_EVENTS } from '@/common/constants'\nimport { imagesSchema } from '@/common/schemas'\nimport { subscribeToEvent } from '@/common/socket'\nimport type { Images } from '@/common/types'\nimport { withZodCatch } from '@/common/utils'\nimport type {\n  CreatePlaylistArgs,\n  FetchPlaylistsArgs,\n  PlaylistCreatedEvent,\n  PlaylistUpdatedEvent,\n  UpdatePlaylistArgs,\n} from '@/features/playlists/api/playlistsApi.types.ts'\nimport { playlistCreateResponseSchema, playlistsResponseSchema } from '@/features/playlists/model/playlists.schemas.ts'\n\nexport const playlistsApi = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    fetchPlaylists: build.query({\n      query: (params: FetchPlaylistsArgs) => ({ url: 'playlists', params }),\n      ...withZodCatch(playlistsResponseSchema),\n      keepUnusedDataFor: 0,\n      onCacheEntryAdded: async (_arg, { cacheDataLoaded, updateCachedData, cacheEntryRemoved }) => {\n        await cacheDataLoaded\n\n        const unsubscribes = [\n          subscribeToEvent<PlaylistCreatedEvent>(SOCKET_EVENTS.PLAYLIST_CREATED, (msg) => {\n            const newPlaylist = msg.payload.data\n            updateCachedData((state) => {\n              state.data.pop()\n              state.data.unshift(newPlaylist)\n              state.meta.totalCount = state.meta.totalCount + 1\n              state.meta.pagesCount = Math.ceil(state.meta.totalCount / state.meta.pageSize)\n            })\n          }),\n          subscribeToEvent<PlaylistUpdatedEvent>(SOCKET_EVENTS.PLAYLIST_UPDATED, (msg) => {\n            const newPlaylist = msg.payload.data\n            updateCachedData((state) => {\n              const index = state.data.findIndex((playlist) => playlist.id === newPlaylist.id)\n              if (index !== -1) {\n                state.data[index] = { ...state.data[index], ...newPlaylist }\n              }\n            })\n          }),\n        ]\n\n        await cacheEntryRemoved\n        unsubscribes.forEach((unsubscribe) => unsubscribe())\n      },\n      providesTags: ['Playlist'],\n    }),\n    createPlaylist: build.mutation({\n      query: (body: CreatePlaylistArgs) => ({ method: 'post', url: 'playlists', body }),\n      ...withZodCatch(playlistCreateResponseSchema),\n      invalidatesTags: ['Playlist'],\n    }),\n    deletePlaylist: build.mutation<void, string>({\n      query: (playlistId) => ({ method: 'delete', url: `playlists/${playlistId}` }),\n      invalidatesTags: ['Playlist'],\n    }),\n    updatePlaylist: build.mutation<void, { playlistId: string; body: UpdatePlaylistArgs }>({\n      query: ({ playlistId, body }) => {\n        return { method: 'put', url: `playlists/${playlistId}`, body }\n      },\n      onQueryStarted: async ({ playlistId, body }, { queryFulfilled, dispatch, getState }) => {\n        const args = playlistsApi.util.selectCachedArgsForQuery(getState(), 'fetchPlaylists')\n\n        const patchCollections: any[] = []\n\n        args.forEach((arg) => {\n          patchCollections.push(\n            dispatch(\n              playlistsApi.util.updateQueryData(\n                'fetchPlaylists',\n                {\n                  pageNumber: arg.pageNumber,\n                  pageSize: arg.pageSize,\n                  search: arg.search,\n                },\n                (state) => {\n                  const index = state.data.findIndex((playlist) => playlist.id === playlistId)\n                  if (index !== -1) {\n                    state.data[index].attributes = { ...state.data[index].attributes, ...body }\n                  }\n                },\n              ),\n            ),\n          )\n        })\n\n        try {\n          await queryFulfilled\n        } catch (e) {\n          patchCollections.forEach((patchCollection) => {\n            patchCollection.undo()\n          })\n        }\n      },\n      invalidatesTags: ['Playlist'],\n    }),\n    uploadPlaylistCover: build.mutation<Images, { playlistId: string; file: File }>({\n      query: ({ playlistId, file }) => {\n        const formData = new FormData()\n        formData.append('file', file)\n        return { method: 'post', url: `playlists/${playlistId}/images/main`, body: formData }\n      },\n      ...withZodCatch(imagesSchema),\n      invalidatesTags: ['Playlist'],\n    }),\n    deletePlaylistCover: build.mutation<void, { playlistId: string }>({\n      query: ({ playlistId }) => ({ method: 'delete', url: `playlists/${playlistId}/images/main` }),\n      invalidatesTags: ['Playlist'],\n    }),\n  }),\n})\n\nexport const {\n  useFetchPlaylistsQuery,\n  useCreatePlaylistMutation,\n  useDeletePlaylistMutation,\n  useUpdatePlaylistMutation,\n  useUploadPlaylistCoverMutation,\n  useDeletePlaylistCoverMutation,\n} = playlistsApi\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/api/playlistsApi.types.ts",
    "content": "import * as z from 'zod'\n\nimport {\n  createPlaylistSchema,\n  playlistAttributesSchema,\n  playlistDataSchema,\n  playlistMetaSchema,\n  playlistsResponseSchema,\n} from '@/features/playlists/model/playlists.schemas.ts'\n\nexport type PlaylistMeta = z.infer<typeof playlistMetaSchema>\nexport type PlaylistAttributes = z.infer<typeof playlistAttributesSchema>\nexport type PlaylistData = z.infer<typeof playlistDataSchema>\nexport type PlaylistsResponse = z.infer<typeof playlistsResponseSchema>\n\n// Arguments\nexport type FetchPlaylistsArgs = {\n  pageNumber?: number\n  pageSize?: number\n  search?: string\n  sortBy?: 'addedAt' | 'likesCount'\n  sortDirection?: 'asc' | 'desc'\n  tagsIds?: string[]\n  userId?: string\n  trackId?: string\n}\n\nexport type CreatePlaylistArgs = z.infer<typeof createPlaylistSchema>\n\nexport type UpdatePlaylistArgs = {\n  title: string\n  description: string\n  tagIds: string[]\n}\n\n// WebSocket Events\nexport type PlaylistCreatedEvent = {\n  type: 'tracks.playlist-created'\n  payload: {\n    data: PlaylistData\n  }\n}\n\nexport type PlaylistUpdatedEvent = {\n  type: 'tracks.playlist-updated'\n  payload: {\n    data: PlaylistData\n  }\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/model/playlists.schemas.ts",
    "content": "import * as z from 'zod'\n\nimport { currentUserReactionSchema, imagesSchema, tagSchema, userSchema } from '@/common/schemas'\n\nexport const createPlaylistSchema = z.object({\n  title: z\n    .string()\n    .min(1, 'The title length must be more than 1 character')\n    .max(100, 'The title length must be less than 100 characters'),\n  description: z.nullable(z.string().max(1000, 'The description length must be less than 1000 characters')),\n})\n\nexport const playlistMetaSchema = z.object({\n  page: z.int().positive(),\n  pageSize: z.int().positive(),\n  totalCount: z.int().positive(),\n  pagesCount: z.int().positive(),\n})\n\nexport const playlistAttributesSchema = z.object({\n  title: z.string(),\n  description: z.string(),\n  addedAt: z.iso.datetime(),\n  updatedAt: z.iso.datetime(),\n  order: z.int(),\n  dislikesCount: z.int().nonnegative(),\n  likesCount: z.int().nonnegative(),\n  tags: z.array(tagSchema),\n  images: imagesSchema,\n  user: userSchema,\n  currentUserReaction: currentUserReactionSchema,\n})\n\nexport const playlistDataSchema = z.object({\n  id: z.string(),\n  type: z.literal('playlists'),\n  attributes: playlistAttributesSchema,\n})\n\nexport const playlistsResponseSchema = z.object({\n  data: z.array(playlistDataSchema),\n  meta: playlistMetaSchema,\n})\n\nexport const playlistCreateResponseSchema = z.object({\n  data: playlistDataSchema,\n})\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/CreatePlaylistForm/CreatePlaylistForm.module.css",
    "content": ".error {\n  color: red;\n  font-weight: bold;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/CreatePlaylistForm/CreatePlaylistForm.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod'\nimport { type SubmitHandler, useForm } from 'react-hook-form'\n\nimport { useCreatePlaylistMutation } from '@/features/playlists/api/playlistsApi.ts'\nimport type { CreatePlaylistArgs } from '@/features/playlists/api/playlistsApi.types.ts'\nimport { createPlaylistSchema } from '@/features/playlists/model/playlists.schemas.ts'\n\nimport s from './CreatePlaylistForm.module.css'\n\nexport const CreatePlaylistForm = () => {\n  const {\n    register,\n    handleSubmit,\n    reset,\n    formState: { errors },\n  } = useForm<CreatePlaylistArgs>({ resolver: zodResolver(createPlaylistSchema) })\n\n  const [createPlaylist] = useCreatePlaylistMutation()\n\n  const onSubmit: SubmitHandler<CreatePlaylistArgs> = (data) => {\n    createPlaylist(data)\n      .unwrap()\n      .then(() => reset())\n  }\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <h2>Create new playlist</h2>\n      <div>\n        <input {...register('title')} placeholder={'title'} />\n        {errors.title && <span className={s.error}>{errors.title.message}</span>}\n      </div>\n      <div>\n        <input {...register('description')} placeholder={'description'} />\n        {errors.description && <span className={s.error}>{errors.description.message}</span>}\n      </div>\n      <button>create playlist</button>\n    </form>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/EditPlaylistForm/EditPlaylistForm.tsx",
    "content": "import type { SubmitHandler, UseFormHandleSubmit, UseFormRegister } from 'react-hook-form'\n\nimport { useUpdatePlaylistMutation } from '@/features/playlists/api/playlistsApi.ts'\nimport type { UpdatePlaylistArgs } from '@/features/playlists/api/playlistsApi.types.ts'\n\ntype Props = {\n  playlistId: string\n  setPlaylistId: (playlistId: null) => void\n  editPlaylist: (playlist: null) => void\n  register: UseFormRegister<UpdatePlaylistArgs>\n  handleSubmit: UseFormHandleSubmit<UpdatePlaylistArgs>\n}\n\nexport const EditPlaylistForm = ({ playlistId, setPlaylistId, editPlaylist, handleSubmit, register }: Props) => {\n  const [updatePlaylist] = useUpdatePlaylistMutation()\n\n  const onSubmit: SubmitHandler<UpdatePlaylistArgs> = (body) => {\n    if (!playlistId) return\n    updatePlaylist({ playlistId, body })\n    setPlaylistId(null)\n  }\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <h2>Edit playlist</h2>\n      <div>\n        <input {...register('title')} placeholder={'title'} />\n      </div>\n      <div>\n        <input {...register('description')} placeholder={'description'} />\n      </div>\n      <button type={'submit'}>save</button>\n      <button type={'button'} onClick={() => editPlaylist(null)}>\n        cancel\n      </button>\n    </form>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/PlaylistItem/PlaylistCover/PlaylistCover.module.css",
    "content": ".cover {\n  width: 240px;\n  height: 240px;\n  object-fit: cover;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/PlaylistItem/PlaylistCover/PlaylistCover.tsx",
    "content": "import type { ChangeEvent } from 'react'\n\nimport defaultCover from '@/assets/images/default-playlist-cover.png'\nimport type { Images } from '@/common/types'\nimport { errorToast } from '@/common/utils'\nimport {\n  useDeletePlaylistCoverMutation,\n  useUploadPlaylistCoverMutation,\n} from '@/features/playlists/api/playlistsApi.ts'\n\nimport s from './PlaylistCover.module.css'\n\ntype Props = {\n  playlistId: string\n  images: Images\n}\n\nexport const PlaylistCover = ({ playlistId, images }: Props) => {\n  const [uploadPlaylistCover] = useUploadPlaylistCoverMutation()\n  const [deletePlaylistCover] = useDeletePlaylistCoverMutation()\n\n  const originalCover = images.main.find((img) => img.type === 'original')\n\n  const src = originalCover ? originalCover.url : defaultCover\n\n  const uploadCoverHandler = (event: ChangeEvent<HTMLInputElement>) => {\n    const maxSize = 1024 * 1024 // 1 MB\n    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']\n\n    const file = event.target.files?.length && event.target.files[0]\n    if (!file) return\n\n    if (!allowedTypes.includes(file.type)) {\n      errorToast('Only JPEG, PNG or GIF images are allowed')\n      return\n    }\n\n    if (file.size > maxSize) {\n      errorToast(`The file is too large. Max size is ${Math.round(maxSize / 1024)} KB`)\n      return\n    }\n\n    uploadPlaylistCover({ playlistId, file })\n  }\n\n  const deleteCoverHandler = () => deletePlaylistCover({ playlistId })\n\n  return (\n    <>\n      <img src={src} alt=\"cover\" width={'240px'} className={s.cover} />\n      <input type=\"file\" accept={'image/jpeg,image/png,image/gif'} onChange={uploadCoverHandler} />\n      {originalCover && <button onClick={deleteCoverHandler}>delete cover</button>}\n    </>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/PlaylistItem/PlaylistDescription/PlaylistDescription.tsx",
    "content": "import type { PlaylistAttributes } from '@/features/playlists/api/playlistsApi.types.ts'\n\ntype Props = {\n  attributes: PlaylistAttributes\n}\n\nexport const PlaylistDescription = ({ attributes }: Props) => {\n  return (\n    <>\n      <div>title: {attributes.title}</div>\n      <div>description: {attributes.description}</div>\n      <div>userName: {attributes.user.name}</div>\n    </>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/PlaylistItem/PlaylistItem.tsx",
    "content": "import type { PlaylistData } from '@/features/playlists/api/playlistsApi.types.ts'\nimport { PlaylistCover } from '@/features/playlists/ui/PlaylistItem/PlaylistCover/PlaylistCover.tsx'\nimport { PlaylistDescription } from '@/features/playlists/ui/PlaylistItem/PlaylistDescription/PlaylistDescription.tsx'\n\ntype Props = {\n  playlist: PlaylistData\n  deletePlaylistHandler: (playlistId: string) => void\n  editPlaylistHandler: (playlist: PlaylistData) => void\n}\n\nexport const PlaylistItem = ({ playlist, editPlaylistHandler, deletePlaylistHandler }: Props) => {\n  return (\n    <div>\n      <PlaylistCover playlistId={playlist.id} images={playlist.attributes.images} />\n      <PlaylistDescription attributes={playlist.attributes} />\n      <button onClick={() => deletePlaylistHandler(playlist.id)}>delete</button>\n      <button onClick={() => editPlaylistHandler(playlist)}>update</button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/PlaylistsList/PlaylistsList.module.css",
    "content": ".items {\n  display: flex;\n  gap: 30px;\n  flex-wrap: wrap;\n}\n\n.item {\n  width: 240px;\n  padding: 16px;\n  border: 1px solid #ddd;\n  border-radius: 8px;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/PlaylistsList/PlaylistsList.tsx",
    "content": "import { useState } from 'react'\nimport { useForm } from 'react-hook-form'\n\nimport { useDeletePlaylistMutation } from '@/features/playlists/api/playlistsApi.ts'\nimport type { PlaylistData, UpdatePlaylistArgs } from '@/features/playlists/api/playlistsApi.types.ts'\nimport { EditPlaylistForm } from '@/features/playlists/ui/EditPlaylistForm/EditPlaylistForm.tsx'\nimport { PlaylistItem } from '@/features/playlists/ui/PlaylistItem/PlaylistItem.tsx'\n\nimport s from './PlaylistsList.module.css'\n\ntype Props = {\n  playlists: PlaylistData[]\n  isPlaylistsLoading: boolean\n}\n\nexport const PlaylistsList = ({ isPlaylistsLoading, playlists }: Props) => {\n  const [playlistId, setPlaylistId] = useState<string | null>(null)\n  const { register, handleSubmit, reset } = useForm<UpdatePlaylistArgs>()\n\n  const [deletePlaylist] = useDeletePlaylistMutation()\n\n  const deletePlaylistHandler = (playlistId: string) => {\n    if (confirm('Are you sure you want to delete the playlist?')) {\n      deletePlaylist(playlistId)\n    }\n  }\n\n  const editPlaylistHandler = (playlist: PlaylistData | null) => {\n    if (playlist) {\n      setPlaylistId(playlist.id)\n      reset({\n        title: playlist.attributes.title,\n        description: playlist.attributes.description,\n        tagIds: playlist.attributes.tags.map((tag) => tag.id),\n      })\n    } else {\n      setPlaylistId(null)\n    }\n  }\n\n  return (\n    <div className={s.items}>\n      {!playlists.length && !isPlaylistsLoading && <h2>Playlists not found</h2>}\n      {playlists.map((playlist) => {\n        const isEditing = playlist.id === playlistId\n\n        return (\n          <div className={s.item} key={playlist.id}>\n            {isEditing ? (\n              <EditPlaylistForm\n                playlistId={playlistId}\n                setPlaylistId={setPlaylistId}\n                editPlaylist={editPlaylistHandler}\n                register={register}\n                handleSubmit={handleSubmit}\n              />\n            ) : (\n              <PlaylistItem\n                playlist={playlist}\n                deletePlaylistHandler={deletePlaylistHandler}\n                editPlaylistHandler={editPlaylistHandler}\n              />\n            )}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/PlaylistsPage.module.css",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/playlists/ui/PlaylistsPage.tsx",
    "content": "import { type ChangeEvent, useState } from 'react'\n\nimport { Pagination } from '@/common/components'\nimport { useDebounceValue } from '@/common/hooks'\nimport { useFetchPlaylistsQuery } from '@/features/playlists/api/playlistsApi.ts'\nimport { PlaylistsList } from '@/features/playlists/ui/PlaylistsList/PlaylistsList.tsx'\n\nimport s from './PlaylistsPage.module.css'\n\nexport const PlaylistsPage = () => {\n  const [search, setSearch] = useState('')\n  const [currentPage, setCurrentPage] = useState(1)\n  const [pageSize, setPageSize] = useState(2)\n\n  const debounceSearch = useDebounceValue(search)\n  const { data, isLoading } = useFetchPlaylistsQuery({\n    search: debounceSearch,\n    pageNumber: currentPage,\n    pageSize,\n  })\n\n  const changePageSizeHandler = (size: number) => {\n    setCurrentPage(1)\n    setPageSize(size)\n  }\n\n  const searchPlaylistHandler = (e: ChangeEvent<HTMLInputElement>) => {\n    setSearch(e.currentTarget.value)\n    setCurrentPage(1)\n  }\n\n  if (isLoading) return <h1>Skeleton loader ...</h1>\n\n  return (\n    <div className={s.container}>\n      <h1>Playlists page</h1>\n      <input type=\"search\" placeholder=\"Search playlist by title\" onChange={(e) => searchPlaylistHandler(e)} />\n      <PlaylistsList isPlaylistsLoading={isLoading} playlists={data?.data || []} />\n      <Pagination\n        currentPage={currentPage}\n        setCurrentPage={setCurrentPage}\n        pagesCount={data?.meta.pagesCount || 1}\n        pageSize={pageSize}\n        changePageSize={changePageSizeHandler}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/tracks/api/tracksApi.ts",
    "content": "import { baseApi } from '@/app/api/baseApi.ts'\nimport { withZodCatch } from '@/common/utils'\nimport type { FetchTracksResponse } from '@/features/tracks/api/tracksApi.types.ts'\nimport { fetchTracksResponseSchema } from '@/features/tracks/model/tracks.schemas.ts'\n\nexport const tracksApi = baseApi.injectEndpoints({\n  endpoints: (build) => ({\n    fetchTracks: build.infiniteQuery<FetchTracksResponse, void, string | null>({\n      infiniteQueryOptions: {\n        initialPageParam: null,\n        getNextPageParam: (lastPage) => {\n          return lastPage.meta.nextCursor || null\n        },\n      },\n      query: ({ pageParam }) => ({\n        url: 'playlists/tracks',\n        params: { cursor: pageParam, paginationType: 'cursor', pageSize: 5 },\n      }),\n      ...withZodCatch(fetchTracksResponseSchema),\n    }),\n  }),\n})\nexport const { useFetchTracksInfiniteQuery } = tracksApi\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/tracks/api/tracksApi.types.ts",
    "content": "import * as z from 'zod'\n\nimport {\n  fetchTracksResponseSchema,\n  trackAttachmentSchema,\n  trackAttributesSchema,\n  trackDataSchema,\n  type trackRelationshipsSchema,\n  tracksIncludedSchema,\n  tracksMetaSchema,\n} from '@/features/tracks/model/tracks.schemas.ts'\n\nexport type TrackAttachment = z.infer<typeof trackAttachmentSchema>\nexport type TrackRelationships = z.infer<typeof trackRelationshipsSchema>\nexport type TrackAttributes = z.infer<typeof trackAttributesSchema>\nexport type TrackData = z.infer<typeof trackDataSchema>\nexport type TracksIncluded = z.infer<typeof tracksIncludedSchema>\nexport type TracksMeta = z.infer<typeof tracksMetaSchema>\nexport type FetchTracksResponse = z.infer<typeof fetchTracksResponseSchema>\n\n// Arguments\nexport type FetchTracksArgs = {\n  pageNumber?: number\n  pageSize?: number\n  search?: string\n  sortBy?: 'publishedAt' | 'likesCount'\n  sortDirection?: 'asc' | 'desc'\n  tagsIds?: string[]\n  artistsIds?: string[]\n  userId?: string\n  includeDrafts?: boolean\n  paginationType?: 'offset' | 'cursor'\n  cursor?: string\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/tracks/model/tracks.schemas.ts",
    "content": "import * as z from 'zod'\n\nimport { currentUserReactionSchema, imagesSchema, userSchema } from '@/common/schemas'\n\nexport const trackAttachmentSchema = z.object({\n  id: z.string(),\n  addedAt: z.iso.datetime(),\n  updatedAt: z.iso.datetime(),\n  version: z.int().nonnegative(),\n  url: z.url(),\n  contentType: z.string(),\n  originalName: z.string(),\n  fileSize: z.int().nonnegative(),\n})\n\nexport const trackRelationshipsSchema = z.object({\n  artists: z.object({\n    data: z.array(\n      z.object({\n        id: z.string(),\n        type: z.literal('artists'),\n      }),\n    ),\n  }),\n})\n\nexport const trackAttributesSchema = z.object({\n  title: z.string(),\n  addedAt: z.iso.datetime(),\n  attachments: z.array(trackAttachmentSchema),\n  images: imagesSchema,\n  currentUserReaction: currentUserReactionSchema,\n  user: userSchema,\n  isPublished: z.boolean(),\n  publishedAt: z.iso.datetime(),\n})\n\nexport const tracksMetaSchema = z.object({\n  nextCursor: z.string().nullable(),\n  page: z.int().positive(),\n  pageSize: z.int().positive(),\n  totalCount: z.int().positive().nullable(),\n  pagesCount: z.int().positive().nullable(),\n})\n\nexport const tracksIncludedSchema = z.object({\n  id: z.string(),\n  type: z.literal('artists'),\n  attributes: z.object({\n    name: z.string(),\n  }),\n})\n\nexport const trackDataSchema = z.object({\n  id: z.string(),\n  type: z.literal('tracks'),\n  attributes: trackAttributesSchema,\n  relationships: trackRelationshipsSchema,\n})\n\nexport const fetchTracksResponseSchema = z.object({\n  data: z.array(trackDataSchema),\n  included: z.array(tracksIncludedSchema),\n  meta: tracksMetaSchema,\n})\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/tracks/ui/LoadingTrigger/LoadingTrigger.tsx",
    "content": "import type { RefObject } from 'react'\n\ntype Props = {\n  observerRef: RefObject<HTMLDivElement | null>\n  isFetchingNextPage: boolean\n}\n\nexport const LoadingTrigger = ({ isFetchingNextPage, observerRef }: Props) => {\n  return (\n    <div ref={observerRef}>\n      {isFetchingNextPage ? <div>Loading more tracks...</div> : <div style={{ height: '20px' }} />}\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/tracks/ui/TracksList/TracksList.module.css",
    "content": ".list {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.item {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 16px;\n  border: 1px solid #ddd;\n  border-radius: 8px;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/tracks/ui/TracksList/TracksList.tsx",
    "content": "import type { TrackData } from '@/features/tracks/api/tracksApi.types.ts'\n\nimport s from './TracksList.module.css'\n\ntype Props = {\n  tracks: TrackData[]\n}\n\nexport const TracksList = ({ tracks }: Props) => {\n  return (\n    <div className={s.list}>\n      {tracks.map((track) => {\n        const { title, user, attachments } = track.attributes\n\n        return (\n          <div key={track.id} className={s.item}>\n            <div>\n              <p>Title: {title}</p>\n              <p>Name: {user.name}</p>\n            </div>\n            {attachments.length ? <audio controls src={attachments[0].url} /> : 'no file'}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/features/tracks/ui/TracksPage.tsx",
    "content": "import { useInfiniteScroll } from '@/common/hooks'\nimport { useFetchTracksInfiniteQuery } from '@/features/tracks/api/tracksApi.ts'\nimport { LoadingTrigger } from '@/features/tracks/ui/LoadingTrigger/LoadingTrigger.tsx'\nimport { TracksList } from '@/features/tracks/ui/TracksList/TracksList.tsx'\n\nexport const TracksPage = () => {\n  const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } = useFetchTracksInfiniteQuery()\n\n  const { observerRef } = useInfiniteScroll({ fetchNextPage, hasNextPage, isFetching })\n\n  const pages = data?.pages.flatMap((page) => page.data) || []\n\n  return (\n    <div>\n      <h1>Tracks page</h1>\n      <TracksList tracks={pages} />\n      {hasNextPage && <LoadingTrigger isFetchingNextPage={isFetchingNextPage} observerRef={observerRef} />}\n      {!hasNextPage && pages.length > 0 && <p>Nothing more to load</p>}\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/index.css",
    "content": ":root {\n  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/main.tsx",
    "content": "import './index.css'\n\nimport { createRoot } from 'react-dom/client'\nimport { Provider } from 'react-redux'\nimport { BrowserRouter } from 'react-router'\n\nimport { store } from '@/app/model/store.ts'\n\nimport { App } from './app/ui/App/App.tsx'\n\ncreateRoot(document.getElementById('root')!).render(\n  <BrowserRouter>\n    <Provider store={store}>\n      <App />\n    </Provider>\n  </BrowserRouter>,\n)\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "youtube/rtk-query/lesson1/vite.config.ts",
    "content": "import react from '@vitejs/plugin-react-swc'\nimport path from 'path'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  resolve: {\n    alias: {\n      '@/': `${path.resolve(__dirname, 'src')}/`,\n    },\n  },\n})\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:\n\n```js\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n\n      // Remove tseslint.configs.recommended and replace with this\n      ...tseslint.configs.recommendedTypeChecked,\n      // Alternatively, use this for stricter rules\n      ...tseslint.configs.strictTypeChecked,\n      // Optionally, add this for stylistic rules\n      ...tseslint.configs.stylisticTypeChecked,\n\n      // Other configs...\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n\nYou can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:\n\n```js\n// eslint.config.js\nimport reactX from 'eslint-plugin-react-x'\nimport reactDom from 'eslint-plugin-react-dom'\n\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n      // Enable lint rules for React\n      reactX.configs['recommended-typescript'],\n      // Enable lint rules for React DOM\n      reactDom.configs.recommended,\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport pluginQuery from '@tanstack/eslint-plugin-query'\nimport { globalIgnores } from 'eslint/config'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config([\n  ...pluginQuery.configs['flat/recommended'],\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs['recommended-latest'],\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite + React + TS</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/app/entrypoint/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/package.json",
    "content": "{\n  \"name\": \"tanstack-query-example\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\",\n    \"api:gen\": \"pnpm  openapi-typescript https://musicfun.it-incubator.app/api-json -o ./src/shared/api/schema.ts --root-types\"\n  },\n  \"dependencies\": {\n    \"@tanstack/react-query\": \"^5.83.0\",\n    \"@tanstack/react-query-devtools\": \"^5.83.0\",\n    \"@tanstack/react-router\": \"^1.127.9\",\n    \"openapi-fetch\": \"^0.14.0\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"react-hook-form\": \"^7.60.0\",\n    \"react-toastify\": \"^11.0.5\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.30.1\",\n    \"@tanstack/eslint-plugin-query\": \"^5.81.2\",\n    \"@tanstack/router-plugin\": \"^1.128.0\",\n    \"@types/react\": \"^19.1.8\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@vitejs/plugin-react\": \"^4.6.0\",\n    \"eslint\": \"^9.30.1\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.20\",\n    \"globals\": \"^16.3.0\",\n    \"openapi-typescript\": \"^7.8.0\",\n    \"typescript\": \"~5.8.3\",\n    \"typescript-eslint\": \"^8.35.1\",\n    \"vite\": \"^7.0.4\"\n  },\n  \"packageManager\": \"pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac\"\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/entrypoint/main.tsx",
    "content": "import '../styles/reset.css'\nimport '../styles/index.css'\n\nimport { QueryClientProvider } from '@tanstack/react-query'\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools'\nimport { RouterProvider } from '@tanstack/react-router'\nimport { createRoot } from 'react-dom/client'\n\nimport { queryClientInstance } from '../tanstack-query/query-client-instance.tsx'\nimport { routerInstance } from '../tanstack-router/router-instance.tsx'\n\ncreateRoot(document.getElementById('root')!).render(\n  <QueryClientProvider client={queryClientInstance}>\n    <RouterProvider router={routerInstance} />\n    <ReactQueryDevtools initialIsOpen={false} />\n  </QueryClientProvider>\n)\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/layouts/root-layout.module.css",
    "content": ".container {\n  padding-top: 10px;\n  max-width: 900px;\n  margin: 0 auto;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/layouts/root-layout.tsx",
    "content": "import 'react-toastify/dist/ReactToastify.css'\n\nimport { Outlet } from '@tanstack/react-router'\nimport { ToastContainer } from 'react-toastify'\n\nimport { AccountBar } from '../../features/auth/ui/account-bar.tsx'\nimport { Header } from '../../shared/ui/header/header.tsx'\nimport styles from './root-layout.module.css'\n\nexport const RootLayout = () => (\n  <>\n    <Header renderAccountBar={() => <AccountBar />} />\n    <div className={styles.container}>\n      <Outlet />\n      <ToastContainer />\n    </div>\n  </>\n)\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/routes/__root.tsx",
    "content": "import { createRootRoute } from '@tanstack/react-router'\n\nimport { RootLayout } from '../layouts/root-layout.tsx'\n\nexport const Route = createRootRoute({\n  component: RootLayout,\n})\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/routes/index.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { PlaylistsPage } from '../../pages/playlists-page.tsx'\n\nexport const Route = createFileRoute('/')({\n  component: PlaylistsPage,\n})\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/routes/my-playlists.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { MyPlaylistsPage } from '../../pages/my-playlists-page.tsx'\n\nexport const Route = createFileRoute('/my-playlists')({\n  component: MyPlaylistsPage,\n})\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/routes/oauth/callback.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { OAuthCallbackPage } from '../../../pages/auth/oauth-callback-page.tsx'\n\nexport const Route = createFileRoute('/oauth/callback')({\n  component: OAuthCallbackPage,\n})\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/routes/routeTree.gen.ts",
    "content": "/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { Route as rootRouteImport } from './__root'\nimport { Route as MyPlaylistsRouteImport } from './my-playlists'\nimport { Route as IndexRouteImport } from './index'\nimport { Route as OauthCallbackRouteImport } from './oauth/callback'\n\nconst MyPlaylistsRoute = MyPlaylistsRouteImport.update({\n  id: '/my-playlists',\n  path: '/my-playlists',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst IndexRoute = IndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst OauthCallbackRoute = OauthCallbackRouteImport.update({\n  id: '/oauth/callback',\n  path: '/oauth/callback',\n  getParentRoute: () => rootRouteImport,\n} as any)\n\nexport interface FileRoutesByFullPath {\n  '/': typeof IndexRoute\n  '/my-playlists': typeof MyPlaylistsRoute\n  '/oauth/callback': typeof OauthCallbackRoute\n}\nexport interface FileRoutesByTo {\n  '/': typeof IndexRoute\n  '/my-playlists': typeof MyPlaylistsRoute\n  '/oauth/callback': typeof OauthCallbackRoute\n}\nexport interface FileRoutesById {\n  __root__: typeof rootRouteImport\n  '/': typeof IndexRoute\n  '/my-playlists': typeof MyPlaylistsRoute\n  '/oauth/callback': typeof OauthCallbackRoute\n}\nexport interface FileRouteTypes {\n  fileRoutesByFullPath: FileRoutesByFullPath\n  fullPaths: '/' | '/my-playlists' | '/oauth/callback'\n  fileRoutesByTo: FileRoutesByTo\n  to: '/' | '/my-playlists' | '/oauth/callback'\n  id: '__root__' | '/' | '/my-playlists' | '/oauth/callback'\n  fileRoutesById: FileRoutesById\n}\nexport interface RootRouteChildren {\n  IndexRoute: typeof IndexRoute\n  MyPlaylistsRoute: typeof MyPlaylistsRoute\n  OauthCallbackRoute: typeof OauthCallbackRoute\n}\n\ndeclare module '@tanstack/react-router' {\n  interface FileRoutesByPath {\n    '/my-playlists': {\n      id: '/my-playlists'\n      path: '/my-playlists'\n      fullPath: '/my-playlists'\n      preLoaderRoute: typeof MyPlaylistsRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/': {\n      id: '/'\n      path: '/'\n      fullPath: '/'\n      preLoaderRoute: typeof IndexRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/oauth/callback': {\n      id: '/oauth/callback'\n      path: '/oauth/callback'\n      fullPath: '/oauth/callback'\n      preLoaderRoute: typeof OauthCallbackRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n  }\n}\n\nconst rootRouteChildren: RootRouteChildren = {\n  IndexRoute: IndexRoute,\n  MyPlaylistsRoute: MyPlaylistsRoute,\n  OauthCallbackRoute: OauthCallbackRoute,\n}\nexport const routeTree = rootRouteImport\n  ._addFileChildren(rootRouteChildren)\n  ._addFileTypes<FileRouteTypes>()\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/styles/index.css",
    "content": "body {\n  font-family:\n    system-ui,\n    -apple-system,\n    BlinkMacSystemFont,\n    'Segoe UI',\n    Roboto,\n    'Helvetica Neue',\n    Arial,\n    sans-serif;\n  background: #060707;\n\n  color: #9c9c9c;\n}\na {\n  color: #9c9c9c;\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/styles/reset.css",
    "content": "/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Prevent font size inflation */\nhtml {\n  -moz-text-size-adjust: none;\n  -webkit-text-size-adjust: none;\n  text-size-adjust: none;\n}\n\n/* Remove default margin in favour of better control in authored CSS */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nfigure,\nblockquote,\ndl,\ndd {\n  margin-block-end: 0;\n}\n\n/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */\nul,\nol {\n  list-style: none;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  line-height: 1.5;\n}\n\n/* Set shorter line heights on headings and interactive elements */\nh1,\nh2,\nh3,\nh4,\nbutton,\ninput,\nlabel {\n  line-height: 1.1;\n}\n\n/* Balance text wrapping on headings */\nh1,\nh2,\nh3,\nh4 {\n  text-wrap: balance;\n}\n\n/* A elements that don't have a class get default styles */\na:not([class]) {\n  text-decoration-skip-ink: auto;\n  color: currentColor;\n}\n\n/* Make images easier to work with */\nimg,\npicture {\n  max-width: 100%;\n  display: block;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n/* Make sure textareas without a rows attribute are not tiny */\ntextarea:not([rows]) {\n  min-height: 10em;\n}\n\n/* Anything that has been anchored to should have extra scroll margin */\n:target {\n  scroll-margin-block: 5ex;\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/tanstack-query/query-client-instance.tsx",
    "content": "import { MutationCache, QueryClient } from '@tanstack/react-query'\n\nimport { mutationGlobalErrorHandler } from '../../shared/ui/util/query-error-handler-for-rhf-factory.ts'\n\nexport type MutationMeta = {\n  /**\n   * Если 'off' — глобальный обработчик ошибок пропускаем,\n   * если 'on' (или нет поля) — вызываем.\n   */\n  globalErrorHandler?: 'on' | 'off'\n}\ndeclare module '@tanstack/react-query' {\n  interface Register {\n    /**\n     * Тип для поля `meta` в useMutation(...)\n     */\n    mutationMeta: MutationMeta\n  }\n}\nexport const queryClientInstance = new QueryClient({\n  mutationCache: new MutationCache({\n    onError: mutationGlobalErrorHandler,\n  }),\n  defaultOptions: {\n    queries: {\n      staleTime: Infinity,\n      refetchOnMount: true,\n      refetchOnWindowFocus: true,\n      refetchOnReconnect: true,\n      gcTime: 5 * 60 * 1000,\n    },\n  },\n})\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/app/tanstack-router/router-instance.tsx",
    "content": "// Create a new router instance\nimport { createRouter } from '@tanstack/react-router'\n\nimport { routeTree } from '../routes/routeTree.gen.ts'\n\nexport const routerInstance = createRouter({ routeTree })\n// Register the router instance for type safety\ndeclare module '@tanstack/react-router' {\n  interface Register {\n    router: typeof routerInstance\n  }\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/auth/api/use-login-mutation.tsx",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { client } from '../../../shared/api/client.ts'\nimport { localStorageKeys } from '../../../shared/config/localstorage-keys.ts'\n\nexport const callbackUrl = 'http://localhost:5173/oauth/callback'\n\nexport const useLoginMutation = () => {\n  const queryClient = useQueryClient()\n\n  const mutation = useMutation({\n    mutationFn: async ({ code }: { code: string }) => {\n      const response = await client.POST('/auth/login', {\n        body: {\n          code: code,\n          redirectUri: callbackUrl,\n          rememberMe: true,\n          accessTokenTTL: '60m',\n        },\n      })\n      if (response.error) {\n        throw new Error(response.error.message)\n      }\n      return response.data\n    },\n    onSuccess: (data) => {\n      localStorage.setItem(localStorageKeys.refreshToken, data.refreshToken)\n      localStorage.setItem(localStorageKeys.accessToken, data.accessToken)\n      queryClient.invalidateQueries({\n        queryKey: ['auth', 'me'],\n      })\n    },\n  })\n\n  return mutation\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/auth/api/use-logout-mutation.tsx",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { client } from '../../../shared/api/client.ts'\n\nexport const useLogoutMutation = () => {\n  const queryClient = useQueryClient()\n\n  const mutation = useMutation({\n    mutationFn: async () => {\n      const response = await client.POST('/auth/logout', {\n        body: {\n          refreshToken: localStorage.getItem('musicfun-refresh-token')!,\n        },\n      })\n      return response.data\n    },\n    onSuccess: () => {\n      localStorage.removeItem('musicfun-refresh-token')\n      localStorage.removeItem('musicfun-access-token')\n      queryClient.resetQueries({\n        queryKey: ['auth', 'me'],\n      })\n    },\n  })\n\n  return mutation\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/auth/api/use-me-query.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { client } from '../../../shared/api/client.ts'\nimport { authKeys } from '../../../shared/api/keys-factories/auth-keys-factory.ts'\n\nexport const useMeQuery = () =>\n  useQuery({\n    queryKey: authKeys.me(),\n    queryFn: async () => {\n      const clientResponse = await client.GET('/auth/me')\n      return clientResponse.data\n    },\n    retry: false,\n  })\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/auth/ui/account-bar.module.css",
    "content": ".meInfoContainer {\n  display: flex;\n  gap: 10px;\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/auth/ui/account-bar.tsx",
    "content": "import { useMeQuery } from '../api/use-me-query.ts'\nimport { CurrentUser } from './current-user/current-user.tsx'\nimport { LoginButton } from './login-button.tsx'\n\nexport const AccountBar = () => {\n  const query = useMeQuery()\n\n  if (query.isPending) return <></>\n\n  return (\n    <div>\n      {!query.data && <LoginButton />}\n      {query.data && <CurrentUser />}\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/auth/ui/current-user/current-user.tsx",
    "content": "import { Link } from '@tanstack/react-router'\n\nimport { useMeQuery } from '../../api/use-me-query.ts'\nimport styles from '../account-bar.module.css'\nimport { LogoutButton } from '../logout-button.tsx'\n\nexport const CurrentUser = () => {\n  const query = useMeQuery()\n\n  if (!query.data) return <span>...</span>\n\n  return (\n    <div className={styles.meInfoContainer}>\n      <Link to=\"/my-playlists\" activeOptions={{ exact: true }}>\n        {query.data!.login} <LogoutButton />\n      </Link>\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/auth/ui/login-button.tsx",
    "content": "import { callbackUrl, useLoginMutation } from '../api/use-login-mutation.tsx'\n\nexport const LoginButton = () => {\n  const mutation = useLoginMutation()\n\n  const handleLoginClick = () => {\n    window.addEventListener('message', handleOauthMessage)\n    window.open(\n      `https://musicfun.it-incubator.app/api/1.0/auth/oauth-redirect?callbackUrl=${callbackUrl}`,\n      'apihub-oauth2',\n      'width=500,height=600'\n    )\n  }\n  const handleOauthMessage = (event: MessageEvent) => {\n    window.removeEventListener('message', handleOauthMessage)\n    if (event.origin !== document.location.origin) {\n      console.warn('origin not match')\n      return\n    }\n    const code = event.data.code\n    if (!code) {\n      console.warn('no code in message')\n      return\n    }\n\n    mutation.mutate({ code })\n  }\n  return <button onClick={handleLoginClick}>Login with APIHUB</button>\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/auth/ui/logout-button.tsx",
    "content": "import { useLogoutMutation } from '../api/use-logout-mutation.tsx'\n\nexport const LogoutButton = () => {\n  const mutation = useLogoutMutation()\n\n  const handleLogoutClick = () => {\n    mutation.mutate()\n  }\n  return <button onClick={handleLogoutClick}>Logout</button>\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/playlists/add-playlist/api/use-add-playlist-mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { client } from '../../../../shared/api/client.ts'\nimport { playlistsKeys } from '../../../../shared/api/keys-factories/playlists-keys-factory.ts'\nimport type { SchemaCreatePlaylistRequestPayload } from '../../../../shared/api/schema.ts'\n\nexport const useAddPlaylistMutation = () => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationFn: async (data: SchemaCreatePlaylistRequestPayload) => {\n      const response = await client.POST('/playlists', {\n        body: data,\n      })\n      return response.data\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: playlistsKeys.lists(),\n        refetchType: 'all',\n      })\n    },\n    meta: { globalErrorHandler: 'on' },\n  })\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/playlists/add-playlist/ui/add-playlist-form.tsx",
    "content": "import { useForm } from 'react-hook-form'\n\nimport type { SchemaCreatePlaylistRequestPayload } from '../../../../shared/api/schema.ts'\nimport { queryErrorHandlerForRHFFactory } from '../../../../shared/ui/util/query-error-handler-for-rhf-factory.ts'\nimport { type JsonApiErrorDocument } from '../../../../shared/util/json-api-error.ts'\nimport { useAddPlaylistMutation } from '../api/use-add-playlist-mutation.ts'\n\nexport const AddPlaylistForm = () => {\n  const {\n    register,\n    handleSubmit,\n    reset,\n    setError,\n    formState: { errors },\n  } = useForm<SchemaCreatePlaylistRequestPayload>()\n\n  const { mutateAsync } = useAddPlaylistMutation()\n\n  const onSubmit = async (data: SchemaCreatePlaylistRequestPayload) => {\n    try {\n      await mutateAsync(data)\n      reset()\n    } catch (error) {\n      queryErrorHandlerForRHFFactory({ setError })(error as unknown as JsonApiErrorDocument)\n    }\n  }\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <h2>Add New Playlist</h2>\n      <p>\n        <input {...register('title')} />\n      </p>\n      {errors.title && <p>{errors.title.message}</p>}\n      <p>\n        <textarea {...register('description')}></textarea>\n      </p>\n      {errors.description && <p>{errors.description.message}</p>}\n\n      <button type={'submit'}>Create</button>\n      {errors.root?.server && <p>{errors.root?.server.message}</p>}\n    </form>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/playlists/delete-playlist/api/use-delete-mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { client } from '../../../../shared/api/client.ts'\nimport { playlistsKeys } from '../../../../shared/api/keys-factories/playlists-keys-factory.ts'\nimport type { SchemaGetPlaylistsOutput } from '../../../../shared/api/schema.ts'\n\nexport const useDeleteMutation = () => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: async (playlistId: string) => {\n      const response = await client.DELETE('/playlists/{playlistId}', {\n        params: { path: { playlistId } },\n      })\n      return response.data\n    },\n    onSuccess: (_, playlistId) => {\n      queryClient.setQueriesData(\n        { queryKey: playlistsKeys.lists() },\n        (oldData: SchemaGetPlaylistsOutput) => {\n          return {\n            ...oldData,\n            data: oldData.data.filter((p) => p.id !== playlistId),\n          }\n        }\n      )\n      queryClient.removeQueries({ queryKey: playlistsKeys.detail(playlistId) })\n    },\n  })\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/playlists/delete-playlist/ui/delete-playlist.tsx",
    "content": "import { useDeleteMutation } from '../api/use-delete-mutation.ts'\n\ntype Props = {\n  playlistId: string\n  onDeleted: (playlistId: string) => void\n}\n\nexport const DeletePlaylist = ({ playlistId, onDeleted }: Props) => {\n  const { mutate } = useDeleteMutation()\n\n  const handleDeleteClick = () => {\n    mutate(playlistId)\n    onDeleted?.(playlistId)\n  }\n\n  return <button onClick={handleDeleteClick}>🗑️</button>\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/playlists/edit-playlist/api/use-playlist-query.tsx",
    "content": "import { useQuery } from '@tanstack/react-query'\n\nimport { client } from '../../../../shared/api/client.ts'\n\nexport const usePlaylistQuery = (playlistId: string | null) => {\n  return useQuery({\n    queryKey: ['playlists', 'details', playlistId],\n    queryFn: async () => {\n      const response = await client.GET('/playlists/{playlistId}', {\n        params: { path: { playlistId: playlistId! } },\n      })\n      return response.data!\n    },\n    enabled: !!playlistId,\n  })\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/playlists/edit-playlist/api/use-update-playlist-mutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\n\nimport { client } from '../../../../shared/api/client.ts'\nimport { playlistsKeys } from '../../../../shared/api/keys-factories/playlists-keys-factory.ts'\nimport type {\n  SchemaGetPlaylistsOutput,\n  SchemaUpdatePlaylistRequestPayload,\n} from '../../../../shared/api/schema.ts'\nimport type { JsonApiErrorDocument } from '../../../../shared/util/json-api-error.ts'\n\ntype MutationVariables = SchemaUpdatePlaylistRequestPayload & { playlistId: string }\n\nexport const useUpdatePlaylistMutation = ({\n  onSuccess,\n  onError,\n}: {\n  onSuccess?: () => void\n  onError?: (error: JsonApiErrorDocument) => void\n}) => {\n  const queryClient = useQueryClient()\n\n  const key = playlistsKeys.myList()\n\n  return useMutation({\n    mutationFn: async (variables: MutationVariables) => {\n      const { playlistId, ...rest } = variables\n      const response = await client.PUT('/playlists/{playlistId}', {\n        params: { path: { playlistId: playlistId } },\n        body: { ...rest, tagIds: [] },\n      })\n      return response.data\n    },\n    onMutate: async (variables: MutationVariables) => {\n      // Cancel any outgoing refetches\n      // (so they don't overwrite our optimistic update)\n      await queryClient.cancelQueries({ queryKey: playlistsKeys.all })\n      // Snapshot the previous value\n      const previousMyPlaylists = queryClient.getQueryData(key)\n      // Optimistically update to the new value\n      queryClient.setQueryData(key, (oldData: SchemaGetPlaylistsOutput) => {\n        return {\n          ...oldData,\n          data: oldData.data.map((p) => {\n            if (p.id === variables.playlistId)\n              return {\n                ...p,\n                attributes: {\n                  ...p.attributes,\n                  description: variables.description,\n                  title: variables.title,\n                },\n              }\n            else return p\n          }),\n        }\n      })\n\n      // Return a context with the previous and new todo\n      return { previousMyPlaylists }\n    },\n    // If the mutation fails, use the context we returned above\n    onError: (error, __: MutationVariables, context) => {\n      queryClient.setQueryData(key, context!.previousMyPlaylists)\n      onError?.(error as unknown as JsonApiErrorDocument)\n    },\n    onSuccess: () => {\n      onSuccess?.()\n    },\n    // Always refetch after error or success:\n    onSettled: (_, __, variables: MutationVariables) => {\n      queryClient.invalidateQueries({\n        queryKey: playlistsKeys.lists(),\n        refetchType: 'all',\n      })\n      queryClient.invalidateQueries({\n        queryKey: playlistsKeys.detail(variables.playlistId),\n        refetchType: 'all',\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/features/playlists/edit-playlist/ui/edit-playlist-form.tsx",
    "content": "import { useEffect } from 'react'\nimport { useForm } from 'react-hook-form'\n\nimport type { SchemaUpdatePlaylistRequestPayload } from '../../../../shared/api/schema.ts'\nimport { queryErrorHandlerForRHFFactory } from '../../../../shared/ui/util/query-error-handler-for-rhf-factory.ts'\nimport { usePlaylistQuery } from '../api/use-playlist-query.tsx'\nimport { useUpdatePlaylistMutation } from '../api/use-update-playlist-mutation.ts'\n\ntype Props = {\n  playlistId: string | null\n  onCancelEditing: () => void\n}\n\nexport const EditPlaylistForm = ({ playlistId, onCancelEditing }: Props) => {\n  const {\n    register,\n    handleSubmit,\n    reset,\n    setError,\n    formState: { errors },\n  } = useForm<SchemaUpdatePlaylistRequestPayload>()\n\n  useEffect(() => {\n    reset()\n  }, [playlistId])\n\n  const { data, isPending, isError } = usePlaylistQuery(playlistId)\n\n  const { mutate } = useUpdatePlaylistMutation({\n    onSuccess: () => {\n      onCancelEditing()\n    },\n    onError: queryErrorHandlerForRHFFactory({ setError }),\n  })\n\n  const onSubmit = (data: SchemaUpdatePlaylistRequestPayload) => {\n    mutate({ ...data, playlistId: playlistId! })\n  }\n\n  const handleCancelEditingClick = () => {\n    onCancelEditing()\n  }\n\n  if (!playlistId) return <></>\n  if (isPending) return <p>Loading...</p>\n  if (isError) return <p>Error...</p>\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <h2>Edit Playlist</h2>\n      <p>\n        <input {...register('title')} defaultValue={data.data.attributes.title} />\n      </p>\n      {errors.title && <p>{errors.title.message}</p>}\n      <p>\n        <textarea\n          {...register('description')}\n          defaultValue={data.data.attributes.description!}></textarea>\n      </p>\n      {errors.description && <p>{errors.description.message}</p>}\n      <button type={'submit'}>Save</button>\n      <button type={'reset'} onClick={handleCancelEditingClick}>\n        Cancel\n      </button>\n      {errors.root?.server && <p>{errors.root?.server.message}</p>}\n    </form>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/pages/auth/oauth-callback-page.tsx",
    "content": "import { useEffect } from 'react'\n\nexport function OAuthCallbackPage() {\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const code = url.searchParams.get('code')\n\n    if (code && window.opener) {\n      window.opener.postMessage({ code }, window.location.origin)\n    }\n\n    window.close()\n  }, [])\n\n  return (\n    <>\n      <h2> OAuth2 Callback page </h2>\n    </>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/pages/my-playlists-page.tsx",
    "content": "import { Navigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { useMeQuery } from '../features/auth/api/use-me-query.ts'\nimport { AddPlaylistForm } from '../features/playlists/add-playlist/ui/add-playlist-form.tsx'\nimport { EditPlaylistForm } from '../features/playlists/edit-playlist/ui/edit-playlist-form.tsx'\nimport { Playlists } from '../widgets/playlists/ui/playlists.tsx'\n\nexport function MyPlaylistsPage() {\n  const { data, isPending } = useMeQuery()\n  const [editingPlaylistId, setEditingPlaylistId] = useState<string | null>(null)\n\n  const handlePlaylistDelete = (playlistId: string) => {\n    if (playlistId === editingPlaylistId) {\n      setEditingPlaylistId(null)\n    }\n  }\n\n  if (isPending) return <div>Loading...</div>\n\n  if (!data) {\n    return <Navigate to=\"/\" replace />\n  }\n\n  return (\n    <div>\n      <h2> My Playlists </h2>\n      <hr />\n      <AddPlaylistForm />\n      <hr />\n      <Playlists\n        userId={data.userId}\n        onPlaylistSelected={(playlistId) => setEditingPlaylistId(playlistId)}\n        onPlaylistDeleted={handlePlaylistDelete}\n      />\n      <hr />\n      <EditPlaylistForm\n        playlistId={editingPlaylistId}\n        onCancelEditing={() => setEditingPlaylistId(null)}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/pages/playlists-page.tsx",
    "content": "import { Playlists } from '../widgets/playlists/ui/playlists.tsx'\n\nexport function PlaylistsPage() {\n  return (\n    <div>\n      <h2> hello it-incubator!!! </h2>\n      <Playlists isSearchActive={true} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/api/client.ts",
    "content": "import createClient, { type Middleware } from 'openapi-fetch'\n\nimport { apiKey, baseUrl } from '../config/api-config.ts'\nimport { localStorageKeys } from '../config/localstorage-keys.ts' // generated by openapi-typescript\nimport type { paths } from './schema'\n\n// mutex\nlet refreshPromise: Promise<void> | null = null\n\nfunction makeRefreshToken() {\n  if (!refreshPromise) {\n    refreshPromise = (async (): Promise<void> => {\n      const refreshToken = localStorage.getItem('musicfun-refresh-token')\n      if (!refreshToken) throw new Error('No refresh token')\n\n      const response = await fetch(baseUrl + 'auth/refresh', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'API-KEY': apiKey,\n        },\n        body: JSON.stringify({\n          refreshToken: refreshToken,\n        }),\n      })\n\n      if (!response.ok) {\n        localStorage.removeItem(localStorageKeys.accessToken)\n        localStorage.removeItem(localStorageKeys.refreshToken)\n        throw new Error('Refresh token failed')\n      }\n      const data = await response.json()\n      localStorage.setItem(localStorageKeys.accessToken, data.accessToken)\n      localStorage.setItem(localStorageKeys.refreshToken, data.refreshToken)\n    })()\n\n    refreshPromise.finally(() => {\n      refreshPromise = null\n    })\n\n    return refreshPromise\n  }\n}\n\nconst authMiddleware: Middleware = {\n  onRequest({ request }) {\n    // set \"foo\" header\n    const accessToken = localStorage.getItem(localStorageKeys.accessToken)\n    if (accessToken) {\n      request.headers.set('Authorization', 'Bearer ' + accessToken)\n    }\n\n    // @ts-expect-error hot fix\n    request._retryRequest = request.clone()\n\n    return request\n  },\n  async onResponse({ request, response }) {\n    if (response.ok) return response\n    if (!response.ok && response.status !== 401) {\n      const errorBody = await response.json()\n      throw errorBody\n    }\n\n    try {\n      await makeRefreshToken()\n      // @ts-expect-error ignore it\n      const originalRequest: Request = request._retryRequest\n      const retryRequest = new Request(originalRequest, {\n        headers: new Headers(originalRequest.headers),\n      })\n      retryRequest.headers.set(\n        'Authorization',\n        'Bearer ' + localStorage.getItem(localStorageKeys.accessToken)\n      )\n      return fetch(retryRequest)\n    } catch {\n      return response\n    }\n  },\n}\n\nexport const client = createClient<paths>({\n  baseUrl: baseUrl,\n  headers: {\n    'api-key': apiKey,\n  },\n})\n\nclient.use(authMiddleware)\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/api/keys-factories/auth-keys-factory.ts",
    "content": "export const authKeys = {\n  all: ['auth'],\n  me: () => [...authKeys.all, 'me'],\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/api/keys-factories/playlists-keys-factory.ts",
    "content": "import type { SchemaGetPlaylistsRequestPayload } from '../schema.ts'\n\nexport const playlistsKeys = {\n  all: ['playlists'],\n  lists: () => [...playlistsKeys.all, 'lists'],\n  myList: () => [...playlistsKeys.lists(), 'my'],\n  list: (filters: Partial<SchemaGetPlaylistsRequestPayload>) => [...playlistsKeys.lists(), filters],\n  details: () => [...playlistsKeys.all, 'details'],\n  detail: (id: string) => [...playlistsKeys.details(), id],\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/api/schema.ts",
    "content": "/**\n * This file was auto-generated by openapi-typescript.\n * Do not make direct changes to the file.\n */\n\nexport interface paths {\n  '/playlists/my': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Get my playlists\n     * @deprecated\n     */\n    get: operations['PlaylistsController_getMyPlaylists']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * Retrieve all playlists\n     * @description Query parameters must conform to the **GetPlaylistsRequestPayload** schema.\n     */\n    get: operations['PlaylistsPublicController_getPlaylists']\n    put?: never\n    /** Create a new playlist */\n    post: operations['PlaylistsController_createPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get a single playlist by ID */\n    get: operations['PlaylistsPublicController_getPlaylistById']\n    /** Update a playlist */\n    put: operations['PlaylistsController_updatePlaylist']\n    post?: never\n    /** Delete a playlist */\n    delete: operations['PlaylistsController_deletePlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Reorder playlists */\n    put: operations['PlaylistsController_reorderPlaylist']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/images/main': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /**\n     * Upload playlist cover\n     * @description Minimum height — 500px; image must be square\n     */\n    post: operations['PlaylistsController_uploadMainImage']\n    /** Delete playlist cover */\n    delete: operations['PlaylistsController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get list of all tracks in all playlists */\n    get: operations['TracksPublicController_getAllTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get list of tracks in a playlist */\n    get: operations['TracksPublicController_getPlaylistTracks']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Get track details by ID */\n    get: operations['TracksPublicController_getTrackDetails']\n    /** Update track information */\n    put: operations['TracksController_updateTrack']\n    post?: never\n    /** Permanently delete a track */\n    delete: operations['TracksController_deleteTrackCompletely']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Like or toggle like on a track */\n    post: operations['TracksPublicController_likeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Dislike or toggle dislike on a track */\n    post: operations['TracksPublicController_dislikeTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove user reaction from a track */\n    delete: operations['TracksPublicController_removeTrackReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/likes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Like a playlist */\n    post: operations['PlaylistsPublicController_likePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/dislikes': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Dislike a playlist */\n    post: operations['PlaylistsPublicController_dislikePlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/reactions': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove user reaction from a playlist */\n    delete: operations['PlaylistsPublicController_removePlaylistReaction']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/tracks/{trackId}/reorder': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    /** Reorder tracks in a playlist */\n    put: operations['TracksController_reorderTrack']\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Add a track to your playlist */\n    post: operations['TracksController_addTrackToPlaylist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/{playlistId}/relationships/tracks/{trackId}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Remove a track from your playlist */\n    delete: operations['TracksController_unbindTrackFromPlaylist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/actions/publish': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Publish a track (make it publicly available) */\n    post: operations['TracksController_publishTrack']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/{trackId}/cover': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Upload track cover */\n    post: operations['TracksController_uploadTrackCover']\n    /** Delete track cover */\n    delete: operations['TracksController_deleteTrackCover']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/playlists/tracks/upload': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a track with MP3 file upload */\n    post: operations['TracksController_uploadTrackMp3']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a new artist */\n    post: operations['ArtistsController_createArtist']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Search artists by substring */\n    get: operations['ArtistsController_searchArtist']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/artists/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Delete an artist by ID */\n    delete: operations['ArtistsController_deleteArtist']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/oauth-redirect': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /**\n     * OAuth редирект\n     * @description The callback URL to redirect after grand access,\n     *          <a target=\"_blank\" href=\"https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid\">https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid</a>\n     */\n    get: operations['AuthController_OauthRedirect']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/login': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Залогиниться с помощью кода, полученного после редиректа после авторизации через OAuth */\n    post: operations['AuthController_login']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/refresh': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Обновить пару refresh/access токенов */\n    post: operations['AuthController_refresh']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/logout': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Деактивировать refresh-token */\n    post: operations['AuthController_logout']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/auth/me': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Получить текущего пользователя по access токену */\n    get: operations['AuthController_getMe']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    /** Create a new tag */\n    post: operations['TagsController_createTag']\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/search': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    /** Search tags by substring */\n    get: operations['TagsController_searchTags']\n    put?: never\n    post?: never\n    delete?: never\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n  '/tags/{id}': {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    get?: never\n    put?: never\n    post?: never\n    /** Delete a tag by ID */\n    delete: operations['TagsController_deleteTag']\n    options?: never\n    head?: never\n    patch?: never\n    trace?: never\n  }\n}\nexport type webhooks = Record<string, never>\nexport interface components {\n  schemas: {\n    UserOutputDTO: {\n      /** @description Unique identifier of the user */\n      id: string\n      /** @description Name of the user */\n      name: string\n    }\n    /**\n     * @description Type of the image size (e.g., original, thumbnail variants)\n     * @enum {string}\n     */\n    ImageSizeType: 'original' | 'thumbnail' | 'medium'\n    ImageDto: {\n      /** @description Type of the image size (e.g., original, thumbnail variants) */\n      type: components['schemas']['ImageSizeType']\n      /** @description Image width in pixels */\n      width: number\n      /** @description Image height in pixels */\n      height: number\n      /** @description Image file size in bytes */\n      fileSize: number\n      /** @description Full public URL of the image */\n      url: string\n    }\n    PlaylistImagesOutputDTO: {\n      /** @description Original images and thumbnail previews */\n      main?: components['schemas']['ImageDto'][]\n    }\n    GetTagOutput: {\n      /** @description Unique identifier of the tag */\n      id: string\n      /** @description Original name of the tag */\n      name: string\n    }\n    /**\n     * @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike\n     * @enum {number}\n     */\n    ReactionValue: 0 | 1 | -1\n    PlaylistAttributesDto: {\n      /** @description Title of the playlist */\n      title: string\n      /** @description Description of the playlist */\n      description: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Order index of the playlist */\n      order: number\n      /** @description User who created the playlist */\n      user: components['schemas']['UserOutputDTO']\n      /** @description Images associated with the playlist */\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      /** @description Tags linked to the playlist */\n      tags: components['schemas']['GetTagOutput'][]\n      /** @description Total number of likes for this playlist */\n      likesCount: number\n      /** @description Total number of dislikes for this playlist */\n      dislikesCount: number\n      /** @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistListItemJsonApiData: {\n      /** @description Unique identifier of the playlist */\n      id: string\n      /**\n       * @description Resource type (should be \"playlists\")\n       * @example playlists\n       */\n      type: string\n      /** @description Attributes of the playlist resource */\n      attributes: components['schemas']['PlaylistAttributesDto']\n    }\n    GetMyPlaylistsOutput: {\n      /** @description Array of playlist resource objects owned by the current user */\n      data: components['schemas']['PlaylistListItemJsonApiData'][]\n    }\n    CreatePlaylistRequestPayload: {\n      /** @description Playlist title (1 to 100 characters) */\n      title: string\n      /** @description Playlist description (up to 1000 characters) */\n      description: string | null\n    }\n    PlaylistOutputAttributes: {\n      /** @description Title of the playlist */\n      title: string\n      /** @description Description of the playlist */\n      description: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the playlist was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Order index of the playlist */\n      order: number\n      /** @description User who created the playlist */\n      user: components['schemas']['UserOutputDTO']\n      /** @description Images associated with the playlist */\n      images: components['schemas']['PlaylistImagesOutputDTO']\n      /** @description Tags linked to the playlist */\n      tags: components['schemas']['GetTagOutput'][]\n      /** @description Total number of likes for this playlist */\n      likesCount: number\n      /** @description Total number of dislikes for this playlist */\n      dislikesCount: number\n      /** @description User reaction: 0 – guest or no reaction; 1 – like; -1 – dislike */\n      currentUserReaction: components['schemas']['ReactionValue']\n    }\n    PlaylistOutput: {\n      /** @description Unique identifier of the playlist */\n      id: string\n      /**\n       * @description Resource type (should be \"playlists\")\n       * @example playlists\n       */\n      type: string\n      /** @description Playlist attributes object */\n      attributes: components['schemas']['PlaylistOutputAttributes']\n    }\n    GetPlaylistOutput: {\n      /** @description JSON:API single-resource response wrapper */\n      data: components['schemas']['PlaylistOutput']\n    }\n    UpdatePlaylistRequestPayload: {\n      /** @description Playlist title (1 – 100 characters) */\n      title: string\n      /**\n       * @description Playlist description (up to 1000 characters)\n       * @example Cool playlist\n       */\n      description: string | null\n      /** @description Tag IDs to associate with the playlist (0 – 5 items; [] = clear tags) */\n      tagIds: string[]\n    }\n    ReorderPlaylistsRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID of the playlist after which the current playlist should be inserted. Send null to place the playlist at the beginning of the list.\n       */\n      putAfterItemId: string | null\n    }\n    GetImagesOutput: {\n      /** @description List of original images and thumbnail versions (e.g., original, 320x180, etc.) */\n      main?: components['schemas']['ImageDto'][]\n    }\n    GetTracksRequestPayload: {\n      /**\n       * @description Page number for pagination (starting from 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Page size for pagination (between 1 and 20)\n       * @default 20\n       */\n      pageSize: number\n      /** @description Search term for filtering playlists by name */\n      search?: string\n      /**\n       * @description Field by which to sort tracks\n       * @default publishedAt\n       * @enum {string}\n       */\n      sortBy: 'publishedAt' | 'likesCount'\n      /**\n       * @description Sort direction (ascending or descending)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: 'asc' | 'desc'\n      /** @description Filter by tag IDs (multiple values allowed) */\n      tagsIds?: string[]\n      /** @description Filter by artist IDs (multiple values allowed) */\n      artistsIds?: string[]\n      /** @description Filter by user ID (track creator's ID) */\n      userId?: string\n      /** @description If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n      includeDrafts?: boolean\n      /**\n       * @description Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination.\n       * @default offset\n       * @enum {string}\n       */\n      paginationType: 'offset' | 'cursor'\n      /** @description Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\". */\n      cursor?: string | null\n    }\n    AttachmentDto: {\n      /** @description Unique identifier of the entity */\n      id: string\n      /**\n       * Format: date-time\n       * @description Date and time when the entity was added\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the entity was last updated\n       */\n      updatedAt: string\n      /** @description Version number of the entity (for concurrency control) */\n      version: number\n      /**\n       * @description Public URL to access the uploaded file\n       * @example https://cdn.example.com/uploads/track123/cover.jpg\n       */\n      url: string\n      /**\n       * @description MIME type of the file\n       * @example image/jpeg\n       */\n      contentType: string\n      /**\n       * @description Original filename uploaded by the user\n       * @example cover.jpg\n       */\n      originalName: string\n      /**\n       * @description Size of the file in bytes\n       * @example 34872\n       */\n      fileSize: number\n    }\n    TrackListItemOutputAttributes: {\n      title: string\n      addedAt: string\n      attachments: components['schemas']['AttachmentDto'][]\n      images: components['schemas']['GetImagesOutput']\n      user: components['schemas']['UserOutputDTO']\n      /**\n       * @description 0 – не залогинен или не реагировал; 1 – лайк; −1 – дизлайк\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n      isPublished: boolean\n      publishedAt?: string\n    }\n    ArtistRelationship: {\n      id: string\n      type: string\n    }\n    ArtistsRelationship: {\n      data: components['schemas']['ArtistRelationship'][]\n    }\n    TrackRelationships: {\n      artists: components['schemas']['ArtistsRelationship']\n    }\n    TrackListItemOutput: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['TrackListItemOutputAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMetaWithPagingAndCursor: {\n      page: number\n      pageSize: number\n      /** @description Total count may be absent when using keyset pagination */\n      totalCount: number | null\n      /** @description Total number of pages */\n      pagesCount: number | null\n      /** @description Cursor for the next page */\n      nextCursor: string | null\n    }\n    OmitTypeClass: {\n      /** @description Name of the artist */\n      name: string\n    }\n    IncludedArtistOutput: {\n      id: string\n      type: string\n      attributes: components['schemas']['OmitTypeClass']\n    }\n    GetTrackListOutput: {\n      data: components['schemas']['TrackListItemOutput'][]\n      meta: components['schemas']['JsonApiMetaWithPagingAndCursor']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    PlaylistTrackAttributes: {\n      /** @description Title of the track */\n      title: string\n      /** @description Order index of the track in the playlist */\n      order: number\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added to the playlist (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated in the playlist (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Attachments related to the track */\n      attachments: components['schemas']['AttachmentDto'][]\n      /** @description Images associated with the track */\n      images: components['schemas']['GetImagesOutput']\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – liked; -1 – disliked\n       * @enum {number|null}\n       */\n      currentUserReaction: 0 | 1 | -1 | null\n    }\n    GetPlaylistTrackListOutputData: {\n      id: string\n      /** @example tracks */\n      type: string\n      attributes: components['schemas']['PlaylistTrackAttributes']\n      relationships: components['schemas']['TrackRelationships']\n    }\n    JsonApiMeta: {\n      totalCount: number\n    }\n    GetPlaylistTrackListOutput: {\n      data: components['schemas']['GetPlaylistTrackListOutputData'][]\n      meta: components['schemas']['JsonApiMeta']\n      included: components['schemas']['IncludedArtistOutput'][]\n    }\n    GetArtistOutput: {\n      /** @description Unique identifier of the artist */\n      id: string\n      /** @description Name of the artist */\n      name: string\n    }\n    TrackDetailsAttributes: {\n      /** @description Track title */\n      title: string\n      /** @description Track lyrics text */\n      lyrics?: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate?: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Duration of the track in seconds */\n      duration: number\n      /** @description Total number of likes for this track */\n      likesCount: number\n      /** @description Total number of dislikes for this track */\n      dislikesCount: number\n      /** @description List of attachments related to the track */\n      attachments: components['schemas']['AttachmentDto'][]\n      /** @description Images associated with the track */\n      images: components['schemas']['GetImagesOutput']\n      /** @description Tags associated with the track */\n      tags: components['schemas']['GetTagOutput'][]\n      /** @description Artists associated with the track */\n      artists: components['schemas']['GetArtistOutput'][]\n      /** @description Publication status of the track */\n      isPublished: boolean\n      /**\n       * Format: date-time\n       * @description Publication date in ISO 8601 format\n       */\n      publishedAt?: string | null\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n    }\n    TrackDetailsData: {\n      /** @description Unique identifier of the track */\n      id: string\n      /**\n       * @description Resource type (should be \"tracks\")\n       * @example tracks\n       */\n      type: string\n      /** @description Detailed attributes of the track resource */\n      attributes: components['schemas']['TrackDetailsAttributes']\n    }\n    GetTrackDetailsOutput: {\n      /** @description JSON:API single-track details response wrapper */\n      data: components['schemas']['TrackDetailsData']\n    }\n    ReactionOutput: {\n      objectId: string\n      /** @enum {number} */\n      value: 0 | 1 | -1\n      likes: number\n      dislikes: number\n    }\n    GetPlaylistsRequestPayload: {\n      /**\n       * @description Page number for pagination (starting from 1)\n       * @default 1\n       */\n      pageNumber: number\n      /**\n       * @description Page size for pagination (between 1 and 20)\n       * @default 20\n       */\n      pageSize: number\n      /** @description Search term for filtering playlists by name */\n      search?: string\n      /**\n       * @description Field by which to sort playlists\n       * @default addedAt\n       * @enum {string}\n       */\n      sortBy: 'addedAt' | 'likesCount'\n      /**\n       * @description Sort direction (ascending or descending)\n       * @default desc\n       * @enum {string}\n       */\n      sortDirection: 'asc' | 'desc'\n      /** @description Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n      tagsIds?: string[]\n      /** @description Filter by user ID (playlist creator’s ID) */\n      userId?: string\n      /** @description Filter by track ID – only playlists containing this track will be returned */\n      trackId?: string\n    }\n    JsonApiMetaWithPaging: {\n      totalCount: number\n      page: number\n      pageSize: number\n      pagesCount: number\n    }\n    GetPlaylistsOutput: {\n      /** @description Array of playlist resource objects */\n      data: components['schemas']['PlaylistListItemJsonApiData'][]\n      /** @description Pagination metadata for the playlists list */\n      meta: components['schemas']['JsonApiMetaWithPaging']\n    }\n    ReorderTracksRequestPayload: {\n      /**\n       * Format: uuid\n       * @description ID of the track after which the current track should be inserted. Send null to place the track at the beginning of the list.\n       * @example a1b2c3d4-e5f6-7890-abcd-1234567890ef\n       */\n      putAfterItemId: string | null\n    }\n    UpdateTrackRequestPayload: {\n      /** @description Track title (1 to 100 characters) */\n      title: string\n      /** @description Track lyrics (up to 5000 characters) */\n      lyrics: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate: string | null\n      /** @description Array of tag IDs to associate with the track (up to 5) */\n      tagIds: string[]\n      /** @description Array of artist IDs to associate with the track (up to 5) */\n      artistsIds: string[]\n    }\n    TrackOutputAttributes: {\n      /** @description Track title */\n      title: string\n      /** @description Track lyrics text */\n      lyrics?: string | null\n      /**\n       * Format: date-time\n       * @description Release date in ISO 8601 format\n       */\n      releaseDate?: string | null\n      /**\n       * Format: date-time\n       * @description Date and time when the track was added (ISO 8601)\n       */\n      addedAt: string\n      /**\n       * Format: date-time\n       * @description Date and time when the track was last updated (ISO 8601)\n       */\n      updatedAt: string\n      /** @description Duration of the track in seconds */\n      duration: number\n      /** @description Total number of likes for this track */\n      likesCount: number\n      /** @description Total number of dislikes for this track */\n      dislikesCount: number\n      /** @description List of attachments related to the track */\n      attachments: components['schemas']['AttachmentDto'][]\n      /** @description Images associated with the track */\n      images: components['schemas']['GetImagesOutput']\n      /** @description Tags associated with the track */\n      tags: components['schemas']['GetTagOutput'][]\n      /** @description Artists associated with the track */\n      artists: components['schemas']['GetArtistOutput'][]\n      /** @description Publication status of the track */\n      isPublished: boolean\n      /**\n       * Format: date-time\n       * @description Publication date in ISO 8601 format\n       */\n      publishedAt?: string | null\n      /**\n       * @description User reaction: 0 – guest or no reaction; 1 – user liked; -1 – user disliked\n       * @enum {number}\n       */\n      currentUserReaction: 0 | 1 | -1\n    }\n    TrackOutput: {\n      /** @description Unique identifier of the track */\n      id: string\n      /**\n       * @description Resource type (should be \"tracks\")\n       * @example tracks\n       */\n      type: string\n      /** @description Attributes of the track resource */\n      attributes: components['schemas']['TrackOutputAttributes']\n    }\n    GetTrackOutput: {\n      /** @description JSON:API single-track response wrapper */\n      data: components['schemas']['TrackOutput']\n    }\n    AddTrackToPlaylistRequestPayload: {\n      /** @description ID of the track to add to the playlist */\n      trackId: string\n    }\n    CreateArtistRequestPayload: {\n      /** @description Artist name (must be between 2 and 30 characters) */\n      name: string\n    }\n    LoginRequestPayload: {\n      /** @description Код, полученный от oauth-сервер после редиректа */\n      code: string\n      /**\n       * @description Укажите тоже значение, что и во время первого запроса на oauth-сервер\n       * @example http://localhost:3000/oauth2/callback\n       */\n      redirectUri: string\n      /**\n       * @description Срок жизни accessToken-а (по дефолту \"3m\"), Можно использовать значение в формате: be a string like \"60s\", \"3m\", \"2h\", \"1d\"\n       * @example 3m\n       */\n      accessTokenTTL?: string\n      /** @description Как долго будет жить refreshToken. Если true - 1 месяц, если false - 30 минут. Явно указанный accessTokenTTL не должен быть больше, чем время жизни refreshToken */\n      rememberMe: boolean\n    }\n    RefreshOutput: {\n      refreshToken: string\n      accessToken: string\n    }\n    BadRequestException: Record<string, never>\n    UnauthorizedException: Record<string, never>\n    RefreshRequestPayload: {\n      refreshToken: string\n    }\n    LogoutRequestPayload: {\n      refreshToken: string\n    }\n    GetMeOutput: {\n      userId: string\n      login: string\n    }\n    CreateTagRequestPayload: {\n      /** @description Tag name (2 to 30 characters) */\n      name: string\n    }\n    /**\n     * Format: binary\n     * @description Файл в multipart/form-data\n     */\n    BinaryFile: string\n  }\n  responses: never\n  parameters: never\n  requestBodies: never\n  headers: never\n  pathItems: never\n}\nexport type SchemaUserOutputDto = components['schemas']['UserOutputDTO']\nexport type SchemaImageSizeType = components['schemas']['ImageSizeType']\nexport type SchemaImageDto = components['schemas']['ImageDto']\nexport type SchemaPlaylistImagesOutputDto = components['schemas']['PlaylistImagesOutputDTO']\nexport type SchemaGetTagOutput = components['schemas']['GetTagOutput']\nexport type SchemaReactionValue = components['schemas']['ReactionValue']\nexport type SchemaPlaylistAttributesDto = components['schemas']['PlaylistAttributesDto']\nexport type SchemaPlaylistListItemJsonApiData = components['schemas']['PlaylistListItemJsonApiData']\nexport type SchemaGetMyPlaylistsOutput = components['schemas']['GetMyPlaylistsOutput']\nexport type SchemaCreatePlaylistRequestPayload =\n  components['schemas']['CreatePlaylistRequestPayload']\nexport type SchemaPlaylistOutputAttributes = components['schemas']['PlaylistOutputAttributes']\nexport type SchemaPlaylistOutput = components['schemas']['PlaylistOutput']\nexport type SchemaGetPlaylistOutput = components['schemas']['GetPlaylistOutput']\nexport type SchemaUpdatePlaylistRequestPayload =\n  components['schemas']['UpdatePlaylistRequestPayload']\nexport type SchemaReorderPlaylistsRequestPayload =\n  components['schemas']['ReorderPlaylistsRequestPayload']\nexport type SchemaGetImagesOutput = components['schemas']['GetImagesOutput']\nexport type SchemaGetTracksRequestPayload = components['schemas']['GetTracksRequestPayload']\nexport type SchemaAttachmentDto = components['schemas']['AttachmentDto']\nexport type SchemaTrackListItemOutputAttributes =\n  components['schemas']['TrackListItemOutputAttributes']\nexport type SchemaArtistRelationship = components['schemas']['ArtistRelationship']\nexport type SchemaArtistsRelationship = components['schemas']['ArtistsRelationship']\nexport type SchemaTrackRelationships = components['schemas']['TrackRelationships']\nexport type SchemaTrackListItemOutput = components['schemas']['TrackListItemOutput']\nexport type SchemaJsonApiMetaWithPagingAndCursor =\n  components['schemas']['JsonApiMetaWithPagingAndCursor']\nexport type SchemaOmitTypeClass = components['schemas']['OmitTypeClass']\nexport type SchemaIncludedArtistOutput = components['schemas']['IncludedArtistOutput']\nexport type SchemaGetTrackListOutput = components['schemas']['GetTrackListOutput']\nexport type SchemaPlaylistTrackAttributes = components['schemas']['PlaylistTrackAttributes']\nexport type SchemaGetPlaylistTrackListOutputData =\n  components['schemas']['GetPlaylistTrackListOutputData']\nexport type SchemaJsonApiMeta = components['schemas']['JsonApiMeta']\nexport type SchemaGetPlaylistTrackListOutput = components['schemas']['GetPlaylistTrackListOutput']\nexport type SchemaGetArtistOutput = components['schemas']['GetArtistOutput']\nexport type SchemaTrackDetailsAttributes = components['schemas']['TrackDetailsAttributes']\nexport type SchemaTrackDetailsData = components['schemas']['TrackDetailsData']\nexport type SchemaGetTrackDetailsOutput = components['schemas']['GetTrackDetailsOutput']\nexport type SchemaReactionOutput = components['schemas']['ReactionOutput']\nexport type SchemaGetPlaylistsRequestPayload = components['schemas']['GetPlaylistsRequestPayload']\nexport type SchemaJsonApiMetaWithPaging = components['schemas']['JsonApiMetaWithPaging']\nexport type SchemaGetPlaylistsOutput = components['schemas']['GetPlaylistsOutput']\nexport type SchemaReorderTracksRequestPayload = components['schemas']['ReorderTracksRequestPayload']\nexport type SchemaUpdateTrackRequestPayload = components['schemas']['UpdateTrackRequestPayload']\nexport type SchemaTrackOutputAttributes = components['schemas']['TrackOutputAttributes']\nexport type SchemaTrackOutput = components['schemas']['TrackOutput']\nexport type SchemaGetTrackOutput = components['schemas']['GetTrackOutput']\nexport type SchemaAddTrackToPlaylistRequestPayload =\n  components['schemas']['AddTrackToPlaylistRequestPayload']\nexport type SchemaCreateArtistRequestPayload = components['schemas']['CreateArtistRequestPayload']\nexport type SchemaLoginRequestPayload = components['schemas']['LoginRequestPayload']\nexport type SchemaRefreshOutput = components['schemas']['RefreshOutput']\nexport type SchemaBadRequestException = components['schemas']['BadRequestException']\nexport type SchemaUnauthorizedException = components['schemas']['UnauthorizedException']\nexport type SchemaRefreshRequestPayload = components['schemas']['RefreshRequestPayload']\nexport type SchemaLogoutRequestPayload = components['schemas']['LogoutRequestPayload']\nexport type SchemaGetMeOutput = components['schemas']['GetMeOutput']\nexport type SchemaCreateTagRequestPayload = components['schemas']['CreateTagRequestPayload']\nexport type SchemaBinaryFile = components['schemas']['BinaryFile']\nexport type $defs = Record<string, never>\nexport interface operations {\n  PlaylistsController_getMyPlaylists: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of playlists retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMyPlaylistsOutput']\n        }\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylists: {\n    parameters: {\n      query?: {\n        /** @description Page number for pagination (starting from 1) */\n        pageNumber?: number\n        /** @description Page size for pagination (between 1 and 20) */\n        pageSize?: number\n        /** @description Search term for filtering playlists by name */\n        search?: string\n        /** @description Field by which to sort playlists */\n        sortBy?: 'addedAt' | 'likesCount'\n        /** @description Sort direction (ascending or descending) */\n        sortDirection?: 'asc' | 'desc'\n        /** @description Filter by tag IDs. Multiple values allowed, e.g.: tagsIds=tag1&tagsIds=tag2 */\n        tagsIds?: string[]\n        /** @description Filter by user ID (playlist creator’s ID) */\n        userId?: string\n        /** @description Filter by track ID – only playlists containing this track will be returned */\n        trackId?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: JSON:API list of playlists with pagination */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistsOutput']\n        }\n      }\n    }\n  }\n  PlaylistsController_createPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Playlist created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Forbidden: Playlist creation limit exceeded */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_getPlaylistById: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the playlist */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Playlist retrieved successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistOutput']\n        }\n      }\n      /** @description Not Found: Playlist with the given ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_updatePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdatePlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Playlist updated successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Bad Request: Validation error (e.g., tag limit exceeded) */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: You do not have permission to update this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deletePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Playlist deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Insufficient permissions to delete this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_reorderPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderPlaylistsRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Playlist order updated successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist or putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_uploadMainImage: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @description Maximum size 1 MB; minimum height 500px; image must be square */\n          file: components['schemas']['BinaryFile']\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Cover uploaded successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetImagesOutput']\n        }\n      }\n      /** @description Bad Request: Invalid image format or dimensions */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No permission to upload cover for this playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Cover deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Removing another user’s playlist cover is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getAllTracks: {\n    parameters: {\n      query?: {\n        /** @description Page number for pagination (starting from 1) */\n        pageNumber?: number\n        /** @description Page size for pagination (between 1 and 20) */\n        pageSize?: number\n        /** @description Search term for filtering playlists by name */\n        search?: string\n        /** @description Field by which to sort tracks */\n        sortBy?: 'publishedAt' | 'likesCount'\n        /** @description Sort direction (ascending or descending) */\n        sortDirection?: 'asc' | 'desc'\n        /** @description Filter by tag IDs (multiple values allowed) */\n        tagsIds?: string[]\n        /** @description Filter by artist IDs (multiple values allowed) */\n        artistsIds?: string[]\n        /** @description Filter by user ID (track creator's ID) */\n        userId?: string\n        /** @description If true, include unpublished tracks (drafts) of current user if userId === currentUserId */\n        includeDrafts?: boolean\n        /** @description Pagination type: \"offset\" for page-number pagination; \"cursor\" for keyset/seek-based pagination. */\n        paginationType?: 'offset' | 'cursor'\n        /** @description Base64-encoded cursor for keyset pagination. Used only if paginationType is \"cursor\". */\n        cursor?: string | null\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Paginated list of tracks */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackListOutput']\n        }\n      }\n    }\n  }\n  TracksPublicController_getPlaylistTracks: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the playlist to retrieve tracks for */\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of tracks in the playlist */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetPlaylistTrackListOutput']\n        }\n      }\n      /** @description Not Found: Playlist with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_getTrackDetails: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the track to retrieve details for */\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Track details with attachments */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackDetailsOutput']\n        }\n      }\n      /** @description Not Found: Track with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_updateTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['UpdateTrackRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Track updated successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackOutput']\n        }\n      }\n      /** @description Bad Request: Tag or artist limit exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Editing another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track or playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCompletely: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track permanently deleted */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Deleting another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_likeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: User reaction recorded and counters updated */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid track ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_dislikeTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: User reaction recorded and counters updated */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid track ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksPublicController_removeTrackReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Reaction removed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_likePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: Like recorded successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid playlist ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_dislikePlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description Created: Dislike recorded successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Bad Request: Invalid playlist ID */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  PlaylistsPublicController_removePlaylistReaction: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Reaction removed successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['ReactionOutput']\n        }\n      }\n      /** @description Unauthorized */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_reorderTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['ReorderTracksRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Track order updated successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Bad Request: Cannot place a track after itself */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track or putAfterItemId not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_addTrackToPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n      }\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['AddTrackToPlaylistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description No Content: Track added to the playlist successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist or track limit exceeded (max 10 tracks) */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_unbindTrackFromPlaylist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        playlistId: string\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track removed from the playlist */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: No access to the playlist */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Playlist not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_publishTrack: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Track published successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Publishing another user’s track is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Track is already published */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the track for which the cover is being uploaded */\n        trackId: string\n      }\n      cookie?: never\n    }\n    /** @description Image file:<br/>\n     *             • Field name — <code>cover</code><br/>\n     *             • Allowed MIME types — <code>image/jpeg</code>, <code>image/png</code>, <code>image/gif</code><br/>\n     *             • Maximum size — <code>100 KB</code> */\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** Format: binary */\n          cover: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Cover uploaded successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetImagesOutput']\n        }\n      }\n      /** @description Bad Request: Invalid file or size exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Cannot upload a cover for another user’s track */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_deleteTrackCover: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        trackId: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Cover deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Removing another user's track cover is not allowed */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Track not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TracksController_uploadTrackMp3: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'multipart/form-data': {\n          /** @example My cool track */\n          title: string\n          /** Format: binary */\n          file: string\n        }\n      }\n    }\n    responses: {\n      /** @description OK: Track created successfully */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTrackOutput']\n        }\n      }\n      /** @description Bad Request: Invalid file format or file size exceeded */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Internal Server Error: Error saving file or track */\n      500: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_createArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateArtistRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Artist created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetArtistOutput']\n        }\n      }\n      /** @description Bad Request: Validation error or invalid input */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Limit of 100 artists per user reached */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Artist with the given name already exists */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  ArtistsController_searchArtist: {\n    parameters: {\n      query: {\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of artists matching the search */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetArtistOutput'][]\n        }\n      }\n    }\n  }\n  ArtistsController_deleteArtist: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Artist deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Artist is attached to tracks or was created by another user */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Artist with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_OauthRedirect: {\n    parameters: {\n      query?: {\n        /** @description The callback URL to redirect after grand access,\n         *          https://oauth.apihub.it-incubator.io/realms/apihub/protocol/openid-connect/auth?client_id=spotifun&response_type=code&redirect_uri=http://localhost:3000/oauth2/callback&scope=openid */\n        callbackUrl?: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Редирект выполнен */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_login: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LoginRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Успешно получена пара токенов */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description BadRequest: Неверный формат запроса или отсутствуют обязательные параметры */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['BadRequestException']\n        }\n      }\n      /** @description Unauthorized: Код недействителен, истёк или не передан, или не совпадает redirectUri */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_refresh: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['RefreshRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: Успешное обновление пары токенов */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['RefreshOutput']\n        }\n      }\n      /** @description Unauthorized: Refresh-token недействителен, истёк или не передан */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['UnauthorizedException']\n        }\n      }\n    }\n  }\n  AuthController_logout: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['LogoutRequestPayload']\n      }\n    }\n    responses: {\n      /** @description OK: refresh токен деактивирован, при этом access-токен остаётся ещё валидным. */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  AuthController_getMe: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: Успешное получение информации о пользователе */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetMeOutput']\n        }\n      }\n      /** @description Unauthorized: access токен отсутствует или недействителен */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_createTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody: {\n      content: {\n        'application/json': components['schemas']['CreateTagRequestPayload']\n      }\n    }\n    responses: {\n      /** @description Created: Tag created successfully */\n      201: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput']\n        }\n      }\n      /** @description Bad Request: Validation error */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Limit of 100 tags per user reached */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Conflict: Tag with the given name already exists */\n      409: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_searchTags: {\n    parameters: {\n      query: {\n        /** @description Substring to search tags by (using normalized name) */\n        search: string\n      }\n      header?: never\n      path?: never\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description OK: List of matching tags */\n      200: {\n        headers: {\n          [name: string]: unknown\n        }\n        content: {\n          'application/json': components['schemas']['GetTagOutput'][]\n        }\n      }\n      /** @description Bad Request: Invalid search query */\n      400: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n  TagsController_deleteTag: {\n    parameters: {\n      query?: never\n      header?: never\n      path: {\n        /** @description ID of the tag to delete */\n        id: string\n      }\n      cookie?: never\n    }\n    requestBody?: never\n    responses: {\n      /** @description No Content: Tag deleted successfully */\n      204: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Unauthorized: User not authenticated */\n      401: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Forbidden: Tag was created by another user or is attached to tracks or playlists */\n      403: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n      /** @description Not Found: Tag with the specified ID not found */\n      404: {\n        headers: {\n          [name: string]: unknown\n        }\n        content?: never\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/config/api-config.ts",
    "content": "export const baseUrl = import.meta.env.VITE_BASE_URL\nexport const apiKey = import.meta.env.VITE_API_KEY\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/config/localstorage-keys.ts",
    "content": "export const localStorageKeys = {\n  refreshToken: 'musicfun-refresh-token',\n  accessToken: 'musicfun-access-token',\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/ui/header/header.module.css",
    "content": ".header {\n  border-bottom: #aaaaaa 1px solid;\n  padding-bottom: 10px;\n}\n\n.container {\n  max-width: 900px;\n  margin: 0 auto;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n}\n\n.linksBlock {\n  display: flex;\n  gap: 10px;\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/ui/header/header.tsx",
    "content": "import { Link } from '@tanstack/react-router'\nimport type { ReactNode } from 'react'\n\nimport styles from './header.module.css'\n\ntype Props = {\n  renderAccountBar: () => ReactNode\n}\n\nexport const Header = ({ renderAccountBar }: Props) => (\n  <header className={styles.header}>\n    <div className={styles.container}>\n      <div className={styles.linksBlock}>\n        <Link to=\"/\">Playlists</Link>\n      </div>\n\n      <div>{renderAccountBar()}</div>\n    </div>\n  </header>\n)\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/ui/pagination/pagination-nav/pagination-nav.module.css",
    "content": ".pagination {\n  display: flex;\n  gap: 8px;\n  justify-content: center;\n}\n\n.pageButton {\n  padding: 4px 10px;\n  background: transparent;\n  border: 1px solid #aaa;\n  border-radius: 4px;\n  cursor: pointer;\n  font-weight: normal;\n  transition:\n    background 0.2s,\n    color 0.2s;\n  color: white;\n}\n\n.pageButtonActive {\n  background: #ececec;\n  font-weight: bold;\n  cursor: default;\n  color: black;\n}\n\n.ellipsis {\n  padding: 4px 10px;\n  user-select: none;\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/ui/pagination/pagination-nav/pagination-nav.tsx",
    "content": "import { getPaginationPages } from '../utils/get-pagination-pages.ts'\nimport s from './pagination-nav.module.css'\n\ntype Props = {\n  current: number\n  pagesCount: number\n  onChange: (page: number) => void\n  isFetching: boolean\n}\n\nconst SIBLING_COUNT = 1\n\nexport const PaginationNav = ({ current, pagesCount, onChange }: Props) => {\n  const pages = getPaginationPages(current, pagesCount, SIBLING_COUNT)\n\n  return (\n    <div className={s.pagination}>\n      {pages.map((item, idx) =>\n        item === '...' ? (\n          <span className={s.ellipsis} key={`ellipsis-${idx}`}>\n            ...\n          </span>\n        ) : (\n          <button\n            key={item}\n            className={item === current ? `${s.pageButton} ${s.pageButtonActive}` : s.pageButton}\n            onClick={() => item !== current && onChange(Number(item))}\n            disabled={item === current}\n            type=\"button\">\n            {item}\n          </button>\n        )\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/ui/pagination/pagination.module.css",
    "content": ".container {\n  display: flex;\n  align-content: center;\n  align-items: center;\n  margin: 0 auto;\n  gap: 40px;\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/ui/pagination/pagination.tsx",
    "content": "import s from './Pagination.module.css'\nimport { PaginationNav } from './pagination-nav/pagination-nav.tsx'\n\ntype Props = {\n  currentPage: number\n  pagesCount: number\n  onPageNumberChange: (page: number) => void\n  isFetching: boolean\n}\n\nexport const Pagination = ({ currentPage, pagesCount, onPageNumberChange, isFetching }: Props) => {\n  return (\n    <div className={s.container}>\n      <PaginationNav\n        current={currentPage}\n        pagesCount={pagesCount}\n        onChange={onPageNumberChange}\n        isFetching={isFetching}\n      />{' '}\n      {isFetching && '⌛️'}\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/ui/pagination/utils/get-pagination-pages.ts",
    "content": "/**\n * Генерирует массив страниц для отображения пагинации с учётом троеточий\n */\nexport const getPaginationPages = (\n  current: number,\n  pagesCount: number,\n  siblingCount: number\n): (number | '...')[] => {\n  if (pagesCount <= 1) return []\n\n  const pages: (number | '...')[] = []\n\n  // Границы диапазона вокруг текущей страницы\n  const leftSibling = Math.max(2, current - siblingCount)\n  const rightSibling = Math.min(pagesCount - 1, current + siblingCount)\n\n  // Всегда показываем первую страницу\n  pages.push(1)\n\n  // Троеточие слева\n  if (leftSibling > 2) {\n    pages.push('...')\n  }\n\n  // Соседние страницы вокруг текущей\n  for (let page = leftSibling; page <= rightSibling; page++) {\n    pages.push(page)\n  }\n\n  // Троеточие справа\n  if (rightSibling < pagesCount - 1) {\n    pages.push('...')\n  }\n\n  // Всегда показываем последнюю страницу (если больше одной)\n  if (pagesCount > 1) {\n    pages.push(pagesCount)\n  }\n\n  return pages\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/ui/util/query-error-handler-for-rhf-factory.ts",
    "content": "import type { FieldValues, Path, UseFormSetError } from 'react-hook-form'\nimport { toast } from 'react-toastify'\n\nimport {\n  isJsonApiErrorDocument,\n  type JsonApiErrorDocument,\n  parseJsonApiErrors,\n} from '../../util/json-api-error.ts'\n\nexport const queryErrorHandlerForRHFFactory = <T extends FieldValues>({\n  setError,\n}: {\n  setError?: UseFormSetError<T>\n}) => {\n  return (err: JsonApiErrorDocument) => {\n    // 400 от сервера в JSON:API формате\n    if (isJsonApiErrorDocument(err)) {\n      const { fieldErrors, globalErrors } = parseJsonApiErrors(err)\n\n      // полевые ошибки\n      for (const [field, message] of Object.entries(fieldErrors)) {\n        setError?.(field as Path<T>, { type: 'server', message })\n      }\n\n      // «глобальные» (без pointer)\n      if (globalErrors.length > 0) {\n        setError?.('root.server', {\n          type: 'server',\n          message: globalErrors.join('\\n'),\n        })\n      }\n    }\n  }\n}\n\nexport const mutationGlobalErrorHandler = (\n  error: Error,\n  _: unknown,\n  __: unknown,\n  mutation: unknown\n) => {\n  // @ts-expect-error look at MutationMeta type\n  if (mutation.meta.globalErrorHandler === 'off') {\n    return\n  }\n\n  if (isJsonApiErrorDocument(error)) {\n    const { globalErrors } = parseJsonApiErrors(error)\n\n    // «глобальные» (без pointer)\n    if (globalErrors.length > 0) {\n      toast(globalErrors.join('\\n'))\n    }\n  }\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/shared/util/json-api-error.ts",
    "content": "export interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport type ExtractError<T> = T extends { error?: infer E } ? E : unknown\n\n/* --- типы ошибок, совпадающие с фильтром -------------------------------- */\nexport interface JsonApiError {\n  status: string\n  code?: string | number\n  title?: string\n  detail?: string\n  source?: { pointer?: string; parameter?: string }\n  meta?: Record<string, unknown>\n}\n\nexport interface JsonApiErrorDocument {\n  errors: JsonApiError[]\n  meta?: Record<string, unknown>\n}\n\nexport function isJsonApiErrorDocument(error: unknown): error is JsonApiErrorDocument {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    // @ts-expect-error type no matter\n    Array.isArray(error.errors)\n  )\n}\n\nexport function parseJsonApiErrors(errorDoc: JsonApiErrorDocument): {\n  fieldErrors: Record<string, string>\n  globalErrors: string[]\n} {\n  const fieldErrors: Record<string, string> = {}\n  const globalErrors: string[] = []\n\n  for (const err of errorDoc.errors) {\n    const msg = err.detail ?? err.title ?? 'Unknown error'\n    const ptr = err.source?.pointer\n    if (ptr) {\n      // убираем префикс JSON:API\n      const field = ptr.replace(/^\\/data\\/attributes\\//, '')\n      fieldErrors[field] = msg\n    } else {\n      globalErrors.push(msg)\n    }\n  }\n\n  return { fieldErrors, globalErrors }\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/widgets/playlists/api/use-playlists-query.ts",
    "content": "import { keepPreviousData, useQuery } from '@tanstack/react-query'\n\nimport { client } from '../../../shared/api/client.ts'\nimport { playlistsKeys } from '../../../shared/api/keys-factories/playlists-keys-factory.ts'\nimport type { SchemaGetPlaylistsRequestPayload } from '../../../shared/api/schema.ts'\n\nexport const usePlaylistsQuery = (\n  userId: string | undefined,\n  filters: Partial<SchemaGetPlaylistsRequestPayload>\n) => {\n  const key = userId ? playlistsKeys.myList() : playlistsKeys.list(filters)\n  const queryParams = userId\n    ? {\n        userId,\n      }\n    : filters\n\n  const query = useQuery({\n    // eslint-disable-next-line @tanstack/query/exhaustive-deps\n    queryKey: key,\n    queryFn: async ({ signal }) => {\n      const response = await client.GET('/playlists', {\n        params: {\n          query: queryParams,\n        },\n        signal,\n      })\n      if (response.error) {\n        throw (response as unknown as { error: Error }).error\n      }\n      return response.data\n    },\n    placeholderData: keepPreviousData,\n  })\n  return query\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/src/widgets/playlists/ui/playlists.tsx",
    "content": "import { useState } from 'react'\n\nimport { DeletePlaylist } from '../../../features/playlists/delete-playlist/ui/delete-playlist.tsx'\nimport { Pagination } from '../../../shared/ui/pagination/pagination.tsx'\nimport { usePlaylistsQuery } from '../api/use-playlists-query.ts'\n\ntype Props = {\n  userId?: string\n  onPlaylistSelected?: (playlistId: string) => void\n  onPlaylistDeleted?: (playlistId: string) => void\n  isSearchActive?: boolean\n}\n\nexport const Playlists = ({\n  userId,\n  onPlaylistSelected,\n  onPlaylistDeleted,\n  isSearchActive,\n}: Props) => {\n  const [pageNumber, setPageNumber] = useState(1)\n  const [search, setSearch] = useState('')\n\n  const query = usePlaylistsQuery(userId, { search, pageNumber })\n\n  const handleSelectPlaylistClick = (playlistId: string) => {\n    onPlaylistSelected?.(playlistId)\n  }\n\n  const handleDeletePlaylist = (playlistId: string) => {\n    onPlaylistDeleted?.(playlistId)\n  }\n\n  if (query.isPending) return <span>Loading...</span>\n  if (query.isError) return <span>Error: {JSON.stringify(query.error.message)}</span>\n\n  return (\n    <div>\n      {isSearchActive && (\n        <>\n          <div>\n            <input\n              value={search}\n              onChange={(e) => setSearch(e.currentTarget.value)}\n              placeholder={'search...'}\n            />\n          </div>\n          <hr />\n        </>\n      )}\n\n      <Pagination\n        pagesCount={query.data.meta.pagesCount}\n        currentPage={pageNumber}\n        onPageNumberChange={setPageNumber}\n        isFetching={query.isFetching}\n      />\n      <ul>\n        {query.data.data.map((playlist) => (\n          <li key={playlist.id}>\n            <span onClick={() => handleSelectPlaylistClick(playlist.id)}>\n              {playlist.attributes.title}\n            </span>{' '}\n            <DeletePlaylist playlistId={playlist.id} onDeleted={handleDeletePlaylist} />\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true,\n    \"noUncheckedIndexedAccess\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"erasableSyntaxOnly\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/tsr.config.json",
    "content": "{\n  \"routesDirectory\": \"./src/app/routes\",\n  \"generatedRouteTree\": \"./src/app/routes/routeTree.gen.ts\"\n}\n"
  },
  {
    "path": "youtube/tanstack-query-router-fsd/lesson1/vite.config.ts",
    "content": "import { tanstackRouter } from '@tanstack/router-plugin/vite'\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [\n    tanstackRouter({\n      target: 'react',\n      autoCodeSplitting: true,\n    }),\n    react(),\n  ],\n})\n"
  }
]