[
  {
    "path": ".codesandbox/ci.json",
    "content": "{\n  \"node\": \"18\",\n  \"sandboxes\": [\"/demo\"]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: http://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.{scss,sass}]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: '🐛 Bug report'\ndescription: Report a reproducible bug or regression\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for reporting an issue :pray:.\n\n        This issue tracker is for reporting reproducible bugs or regression's found in [react-spotify-web-playback](https://github.com/gilbarbara/react-spotify-web-playback)\n        If you have a question about how to achieve something and are struggling, please post a question\n        inside of react-spotify-web-playback's [Discussions tab](https://github.com/gilbarbara/react-spotify-web-playback/discussions)\n\n        Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:\n         - [Discussions tab](https://github.com/gilbarbara/react-spotify-web-playback/discussions)\n         - [Open Issues](https://github.com/gilbarbara/react-spotify-web-playback/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)\n         - [Closed Issues](https://github.com/gilbarbara/react-spotify-web-playback/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)\n\n        The more information you fill in, the better the community can help you.\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the bug\n      description: Provide a clear and concise description of the challenge you are running into.\n    validations:\n      required: true\n  - type: input\n    id: link\n    attributes:\n      label: Your minimal, reproducible example\n      description: |\n        Please add a link to a minimal reproduction.\n        Note:\n        - Your bug may get fixed much faster if we can run your code.\n        - To create a shareable code example for web, you can use CodeSandbox (https://codesandbox.io/s/new) or Stackblitz (https://stackblitz.com/).\n        - Please make sure the example is complete and runnable - e.g. avoid localhost URLs.\n        - Feel free to fork the demo CodeSandbox example to reproduce your issue: https://codesandbox.io/s/github/gilbarbara/react-spotify-web-playback/main/demo\n      placeholder: |\n        e.g. Code Sandbox, Stackblitz, etc.\n    validations:\n      required: true\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to reproduce\n      description: Describe the steps we have to take to reproduce the behavior.\n      placeholder: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. See error\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected behavior\n      description: Provide a clear and concise description of what you expected to happen.\n      placeholder: |\n        As a user, I expected ___ behavior but i am seeing ___\n    validations:\n      required: true\n  - type: dropdown\n    attributes:\n      label: How often does this bug happen?\n      description: |\n        Following the reproduction steps above, how easily are you able to reproduce this bug?\n      options:\n        - Every time\n        - Often\n        - Sometimes\n        - Only once\n  - type: textarea\n    id: screenshots_or_videos\n    attributes:\n      label: Screenshots or Videos\n      description: |\n        If applicable, add screenshots or a video to help explain your problem.\n        For more information on the supported file image/file types and the file size limits, please refer\n        to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files\n      placeholder: |\n        You can drag your video or image files inside of this editor ↓\n  - type: textarea\n    id: platform\n    attributes:\n      label: Platform\n      description: |\n        If the problem is specific to a platform, please let us know which one.\n      placeholder: |\n        - OS: [e.g. macOS, Windows, Linux, iOS, Android]\n        - Browser: [e.g. Chrome, Safari, Firefox, React Native]\n        - Version: [e.g. 91.1]\n  - type: input\n    id: rswp-version\n    attributes:\n      label: react-spotify-web-playback version\n      description: |\n        Please let us know the exact version of react-spotify-web-playback you were using when the issue occurred. Please don't just put in \"latest\", as this is subject to change.\n      placeholder: |\n        e.g. 0.14.0\n    validations:\n      required: true\n  - type: input\n    id: ts-version\n    attributes:\n      label: TypeScript version\n      description: |\n        If you are using TypeScript, please let us know the exact version of TypeScript you were using when the issue occurred.\n      placeholder: |\n        e.g. 5.1.0\n  - type: input\n    id: build-tool\n    attributes:\n      label: Build tool\n      description: |\n        If the issue is specific to a build tool, please let us know which one.\n      placeholder: |\n        e.g. webpack, vite, rollup, parcel, create-react-app, next.js, etc.\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional context\n      description: Add any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 🗣 Feature Request / Question / Help\n    url: https://github.com/gilbarbara/react-spotify-web-playback/discussions/new\n    about: How does it work with...? I have an idea...\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: ['main']\n    tags: ['v*']\n  pull_request:\n    branches: ['*']\n\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  main:\n    name: Validate and Deploy\n    runs-on: ubuntu-latest\n\n    env:\n      CI: true\n\n    steps:\n      - name: Setup timezone\n        uses: zcong1993/setup-timezone@master\n        with:\n          timezone: America/Sao_Paulo\n\n      - name: Setup repo\n        uses: actions/checkout@v4\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v3\n        with:\n          version: 9\n          run_install: false\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - name: Setup pnpm cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: \"${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\"\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Install Packages\n        run: pnpm install\n        timeout-minutes: 3\n\n      - name: Validate and Build\n        if: \"!startsWith(github.ref, 'refs/tags/')\"\n        run: pnpm run validate\n        timeout-minutes: 3\n\n      - name: SonarCloud Scan\n        if: \"!startsWith(github.ref, 'refs/tags/')\"\n        uses: SonarSource/sonarqube-scan-action@master\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}\n\n      - name: Publish Package\n        if: startsWith(github.ref, 'refs/tags/')\n        run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.tmp/\ncoverage/\ndemo/pnpm-lock.yaml\ndist/\nnode_modules/\n"
  },
  {
    "path": ".husky/post-merge",
    "content": "./node_modules/.bin/repo-tools install-packages\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "./node_modules/.bin/repo-tools check-remote && npm run validate\n"
  },
  {
    "path": ".prettierignore",
    "content": "coverage\nlib\nnode_modules\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to react-spotify-web-playback\n\n:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:\n\n**Reporting Bugs**  \nBefore creating bug reports, please check this [list](https://github.com/gilbarbara/react-spotify-web-playback/issues) as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible.\n\n**Pull Requests**  \nBefore submitting a new pull request, open a new issue to discuss it. It may already been implemented but not published or we might have found the same situation before and decide against it.\n\nIn any case:\n\n- Format files using these rules [EditorConfig](https://github.com/gilbarbara/react-spotify-web-playback/blob/main/.editorconfig)\n- Follow the [ESLint](https://github.com/gilbarbara/eslint-config) styleguide.\n\nThank you!\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019, Gil Barbara\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# react-spotify-web-playback\n\n[![npm version](https://badge.fury.io/js/react-spotify-web-playback.svg)](https://www.npmjs.com/package/react-spotify-web-playback) [![CI](https://github.com/gilbarbara/react-spotify-web-playback/actions/workflows/main.yml/badge.svg)](https://github.com/gilbarbara/react-spotify-web-playback/actions/workflows/main.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gilbarbara_react-spotify-web-playback&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-spotify-web-playback) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=gilbarbara_react-spotify-web-playback&metric=coverage)](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-spotify-web-playback)\n\n#### A Spotify player with [Spotify's Web Playback SDK](https://developer.spotify.com/documentation/web-playback-sdk/).\n\nView the [demo](https://react-spotify-web-playback.gilbarbara.dev/)\n\nCheck the [supported browser](https://developer.spotify.com/documentation/web-playback-sdk/#supported-browsers) list. This library will try to use the user's devices to work with unsupported browsers.\n\n## Setup\n\n```bash\nnpm i react-spotify-web-playback\n```\n\n## Getting Started\n\n```jsx\nimport SpotifyPlayer from 'react-spotify-web-playback';\n\n<SpotifyPlayer\n  token=\"BQAI_7RWPJuqdZxS-I8XzhkUi9RKr8Q8UUNaJAHwWlpIq6...\"\n  uris={['spotify:artist:6HQYnRM4OzToCYPpVBInuU']}\n/>;\n```\n\n### Client-side only\n\nThis library requires the `window` object.  \nIf you are using an SSR framework, you'll need to use a [dynamic import](https://nextjs.org/docs/advanced-features/dynamic-import) or a [Client Component](https://beta.nextjs.org/docs/rendering/server-and-client-components#client-components) to load the player.\n\n## Spotify Token\n\nIt needs a Spotify token with the following scopes:\n\n- streaming\n- user-read-email\n- user-read-private\n- user-read-playback-state (to read other devices' status)\n- user-modify-playback-state (to update other devices)\n\nIf you want to show the Favorite button (💚), you'll need the additional scopes:\n\n- user-library-read\n- user-library-modify\n\nPlease refer to Spotify's Web API [docs](https://developer.spotify.com/documentation/web-api/) for more information.\n\n> This library doesn't handle token generation and expiration. You'll need to handle that by yourself.\n\n## Props\n\n**callback** `(state: CallbackState) => void`  \nGet status updates from the player.\n\n<details>\n  <summary>Type Definition</summary>\n\n```typescript\ntype ErrorType = 'account' | 'authentication' | 'initialization' | 'playback' | 'player';\ntype RepeatState = 'off' | 'context' | 'track';\ntype Status = 'ERROR' | 'IDLE' | 'INITIALIZING' | 'READY' | 'RUNNING' | 'UNSUPPORTED';\ntype Type =\n  | 'device_update'\n  | 'favorite_update'\n  | 'player_update'\n  | 'progress_update'\n  | 'status_update'\n  | 'track_update';\n\ninterface CallbackState extends State {\n  type: Type;\n}\n\ninterface State {\n  currentDeviceId: string;\n  deviceId: string;\n  devices: SpotifyDevice[];\n  error: string;\n  errorType: ErrorType | null;\n  isActive: boolean;\n  isInitializing: boolean;\n  isMagnified: boolean;\n  isPlaying: boolean;\n  isSaved: boolean;\n  isUnsupported: boolean;\n  needsUpdate: boolean;\n  nextTracks: SpotifyTrack[];\n  playerPosition: 'bottom' | 'top';\n  position: number;\n  previousTracks: SpotifyTrack[];\n  progressMs: number;\n  repeat: RepeatState;\n  shuffle: boolean;\n  status: Status;\n  track: SpotifyTrack;\n  volume: number;\n}\n```\n\n</details>\n\n**components** `CustomComponents`  \nCustom components for the player.\n\n<details>\n  <summary>Type Definition</summary>\n\n```typescript\ninterface CustomComponents {\n  /**\n   * A React component to be displayed before the previous button.\n   */\n  leftButton?: ReactNode;\n  /**\n   * A React component to be displayed after the next button.\n   */\n  rightButton?: ReactNode;\n}\n```\n\n</details>\n\n**getOAuthToken** `(callback: (token: string) => void) => Promise<void>`  \nThe callback [Spotify SDK](https://developer.spotify.com/documentation/web-playback-sdk/reference/#initializing-the-sdk) uses to get/update the token.  \n _Use it to generate a new token when the player needs it._\n\n<details>\n  <summary>Example</summary>\n\n```tsx\nimport { useState } from 'react';\nimport SpotifyPlayer, { Props } from 'react-spotify-web-playback';\n\nimport { refreshTokenRequest } from '../some_module';\n\nexport default function PlayerWrapper() {\n  const [accessToken, setAccessToken] = useState('');\n  const [refreshToken, setRefreshToken] = useState('');\n  const [expiresAt, setExpiresAt] = useState(0);\n\n  const getOAuthToken: Props['getOAuthToken'] = async callback => {\n    if (expiresAt > Date.now()) {\n      callback(accessToken);\n\n      return;\n    }\n\n    const { acess_token, expires_in, refresh_token } = await refreshTokenRequest(refreshToken);\n\n    setAccessToken(acess_token);\n    setRefreshToken(refresh_token);\n    setExpiresAt(Date.now() + expires_in * 1000);\n\n    callback(acess_token);\n  };\n\n  return <SpotifyPlayer getOAuthToken={getOAuthToken} token={accessToken} uris={[]} />;\n}\n```\n\n</details>\n\n**getPlayer** `(player: SpotifyPlayer) => void`  \nGet the Spotify Web Playback SDK instance.\n\n**hideAttribution** `boolean` ▶︎ false  \nHide the Spotify logo.\n\n**hideCoverArt** `boolean` ▶︎ false  \nHide the cover art\n\n**initialVolume** `number` between 0 and 1. ▶︎ 1  \nThe initial volume for the player. It's not used for external devices.\n\n**inlineVolume** `boolean` ▶︎ true  \nShow the volume inline for the \"responsive\" layout for 768px and above.\n\n**layout** `'compact' | 'responsive'` ▶︎ 'responsive'  \nThe layout of the player.\n\n**locale** `Locale`  \nThe strings used for aria-label/title attributes.\n\n<details>\n  <summary>Type Definition</summary>\n\n```typescript\ninterface Locale {\n  currentDevice?: string; // 'Current device'\n  devices?: string; // 'Devices'\n  next?: string; // 'Next'\n  otherDevices?: string; // 'Select other device'\n  pause?: string; // 'Pause'\n  play?: string; // 'Play'\n  previous?: string; // 'Previous'\n  removeTrack?: string; // 'Remove from your favorites'\n  saveTrack?: string; // 'Save to your favorites'\n  title?: string; // '{name} on SPOTIFY'\n  volume?: string; // 'Volume'\n}\n```\n\n</details>\n\n**magnifySliderOnHover**: `boolean` ▶︎ false  \nMagnify the player's slider on hover.\n\n**name** `string` ▶︎ 'Spotify Web Player'  \nThe name of the player.\n\n**offset** `number`  \nThe position of the list/tracks you want to start the player.\n\n**persistDeviceSelection** `boolean` ▶︎ false  \nSave the device selection.\n\n**play** `boolean`  \nControl the player's status.\n\n**preloadData** `boolean`  \nPreload the track data before playing.\n\n**showSaveIcon** `boolean` ▶︎ false  \nDisplay a Favorite button. It needs additional scopes in your token.\n\n**styles** `object`  \nCustomize the player's appearance. Check `StylesOptions` in the [types](src/types/common.ts).\n\n**syncExternalDevice** `boolean` ▶︎ false  \nUse the external player context if there are no URIs and an external device is playing.\n\n**syncExternalDeviceInterval** `number` ▶︎ 5  \nThe time in seconds that the player will sync with external devices.\n\n**token** `string` **REQUIRED**  \nA Spotify token. More info is below.\n\n**updateSavedStatus** `(fn: (status: boolean) => any) => any`  \nProvide you with a function to sync the track saved status in the player.  \n_This works in addition to the **showSaveIcon** prop, and it is only needed if you keep track's saved status in your app._\n\n**uris** `string | string[]` **REQUIRED**  \nA list of Spotify [URIs](https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids).\n\n## Spotify API\n\nThe functions that interact with the Spotify API are exported for your convenience.  \nUse them at your own risk.\n\n```tsx\nimport { spotifyApi } from 'react-spotify-web-playback';\n```\n\n**checkTracksStatus(token: string, tracks: string | string[]): Promise\\<boolean[]>**\n\n**getAlbumTracks(token: string, id: string): Promise\\<SpotifyApi.AlbumTracksResponse>**\n\n**getArtistTopTracks(token: string, id: string): Promise\\<SpotifyApi.ArtistsTopTracksResponse>**\n\n**getDevices(token: string): Promise\\<SpotifyApi.UserDevicesResponse>**\n\n**getPlaybackState(token: string): Promise\\<SpotifyApi.CurrentlyPlayingObject | null>**\n\n**getPlaylistTracks(token: string, id: string): Promise\\<SpotifyApi.PlaylistTrackResponse>**\n\n**getQueue(token: string): Promise\\<SpotifyApi.UsersQueueResponse>**\n\n**getShow(token: string, id: string): Promise\\<SpotifyApi.ShowObjectFull>**\n\n**getShowEpisodes(token: string, id: string, offset = 0): Promise\\<SpotifyApi.ShowEpisodesResponse>**\n\n**getTrack(token: string, id: string): Promise\\<SpotifyApi.TrackObjectFull>**\n\n**pause(token: string, deviceId?: string): Promise\\<void>**\n\n**play(token: string, options: SpotifyPlayOptions): Promise\\<void>**\n\n```typescript\ninterface SpotifyPlayOptions {\n  context_uri?: string;\n  deviceId: string;\n  offset?: number;\n  uris?: string[];\n}\n```\n\n**previous(token: string, deviceId?: string): Promise\\<void>**\n\n**next(token: string, deviceId?: string): Promise\\<void>**\n\n**removeTracks(token: string, tracks: string | string[]): Promise\\<void>**\n\n**repeat(token: string, state: 'context' | 'track' | 'off', deviceId?: string): Promise\\<void>**\n\n**saveTracks(token: string, tracks: string | string[]): Promise\\<void>**\n\n**seek(token: string, position: number, deviceId?: string): Promise\\<void>**\n\n**setDevice(token: string, deviceId: string, shouldPlay?: boolean): Promise\\<void>**\n\n**setVolume(token: string, volume: number, deviceId?: string): Promise\\<void>**\n\n**shuffle(token: string, state: boolean, deviceId?: string): Promise\\<void>**\n\n## Styling\n\nYou can customize the UI with a `styles` prop.  \nIf you want a transparent player, you can use `bgColor: 'transparent'`.  \nCheck all the available options [here](src/types/common.ts#L195).\n\n```tsx\n<SpotifyWebPlayer\n  // ...\n  styles={{\n    activeColor: '#fff',\n    bgColor: '#333',\n    color: '#fff',\n    loaderColor: '#fff',\n    sliderColor: '#1cb954',\n    trackArtistColor: '#ccc',\n    trackNameColor: '#fff',\n  }}\n/>\n```\n\n![rswp-styles](https://files.gilbarbara.dev/media/rswp-v2.png)\n\n\n## Issues\n\nIf you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/gilbarbara/react-spotify-web-playback/issues).\n\n## License\n\nMIT\n"
  },
  {
    "path": "demo/package.json",
    "content": "{\n  \"name\": \"react-spotify-web-playback-demo\",\n  \"version\": \"0.14.4\",\n  \"description\": \"Demo for react-spotify-web-playback\",\n  \"keywords\": [],\n  \"main\": \"src/index.tsx\",\n  \"dependencies\": {\n    \"@emotion/react\": \"^11.14.0\",\n    \"@emotion/styled\": \"^11.14.0\",\n    \"@gilbarbara/hooks\": \"^0.9.0\",\n    \"@gilbarbara/components\": \"^0.15.1\",\n    \"@gilbarbara/cookies\": \"^1.0.1\",\n    \"@gilbarbara/eslint-config\": \"^0.8.4\",\n    \"@gilbarbara/helpers\": \"^0.9.5\",\n    \"@gilbarbara/prettier-config\": \"^1.0.0\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-scripts\": \"^5.0.1\",\n    \"react-spotify-web-playback\": \"latest\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.10.6\",\n    \"@types/react\": \"^18.3.12\",\n    \"@types/react-dom\": \"^18.3.1\",\n    \"typescript\": \"5.7.3\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": \"@gilbarbara/eslint-config\"\n  },\n  \"prettier\": \"@gilbarbara/prettier-config\",\n  \"browserslist\": [\n    \">0.2%\",\n    \"not dead\",\n    \"not ie <= 11\",\n    \"not op_mini all\"\n  ]\n}\n"
  },
  {
    "path": "demo/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"\n    />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React Spotify Web Playback</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "demo/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "demo/src/App.tsx",
    "content": "import {\n  DragEventHandler,\n  FormEvent,\n  MouseEvent,\n  ReactNode,\n  useCallback,\n  useEffect,\n  useRef,\n} from 'react';\nimport SpotifyWebPlayer, {\n  CallbackState,\n  ERROR_TYPE,\n  Layout,\n  RepeatState,\n  SpotifyPlayer,\n  STATUS,\n  StylesProps,\n  TYPE,\n  Type,\n} from 'react-spotify-web-playback';\nimport {\n  Anchor,\n  Box,\n  Button,\n  ButtonGroup,\n  ButtonUnstyled,\n  Container,\n  Flex,\n  FormElementWrapper,\n  H1,\n  H4,\n  Icon,\n  Input,\n  Loader,\n  NonIdealState,\n  Paragraph,\n  Spacer,\n  Toggle,\n} from '@gilbarbara/components';\nimport { request } from '@gilbarbara/helpers';\nimport { useEffectOnce, useSetState } from '@gilbarbara/hooks';\n\nimport GlobalStyles from './components/GlobalStyles';\nimport Player from './components/Player';\nimport RepeatButton from './components/RepeatButton';\nimport ShuffleButton from './components/ShuffleButton';\nimport {\n  getAuthorizeUrl,\n  getCredentials,\n  login,\n  logout,\n  parseURIs,\n  refreshCredentials,\n  setCredentials,\n} from './modules/helpers';\n\ninterface State {\n  accessToken: string;\n  error?: string;\n  hideAttribution: boolean;\n  inlineVolume: boolean;\n  isActive: boolean;\n  isPlaying: boolean;\n  layout: 'responsive' | 'compact';\n  player: SpotifyPlayer | null;\n  refreshToken: string;\n  repeat: RepeatState;\n  shuffle: boolean;\n  styles?: StylesProps;\n  transparent: boolean;\n  URIs: string[];\n}\n\nconst baseURIs = {\n  // album: 'spotify:album:0WLIcGHr0nLyKJpMirAS17', // The Breathing Effect - Mars Is A Very Bad Place For Love\n  album: 'spotify:album:4c7fP0tUymaZcrEFIeIeZc', // Caribou - Honey\n  // artist: 'spotify:artist:4oLeXFyACqeem2VImYeBFe', // Fred Again..\n  artist: 'spotify:artist:7A0awCXkE1FtSU8B0qwOJQ', // Jamie xx\n  // playlist: 'spotify:playlist:1Zr2FUPeD5hYJTGbTDSQs4', // Rework\n  playlist: 'spotify:playlist:3h7lEfRkEdtVvGJTdTAudn', // Nation\n  show: 'spotify:show:4kYCRYJ3yK5DQbP5tbfZby',\n  tracks: [\n    // Boogie\n    // 'spotify:track:3zYpRGnnoegSpt3SguSo3W',\n    // 'spotify:track:5sjeJXROHuutyj8P3JGZoN',\n    // 'spotify:track:3u0VPnYkZo30zw60SInouA',\n    // 'spotify:track:5ZoDwIP1ntHwciLjydJ8X2',\n    // 'spotify:track:7ohR0qPH6f2Vuj2pUNanJG',\n    // 'spotify:track:5g2sPpVq3hdk9ZuMfABrts',\n    // 'spotify:track:3mJ6pNcFM2CkykCYSREdKT',\n    // 'spotify:track:63DTXKZi7YdJ4tzGti1Dtr',\n\n    // 90s Electronic\n    // 'spotify:track:5Kh3pqvJGVCBapAgrRP8QO',\n    // 'spotify:track:0j5FJJOmmnXPd0XajFWkMF',\n    // 'spotify:track:3XWgwgbWDI56mf1Wl3cLzb',\n    // 'spotify:track:6rvinglzwGWPaO9N9nnHeR',\n    // 'spotify:track:6LERtd1yiclxFH8MHAqr0Q',\n    // 'spotify:track:5eFCFpmDbqGqpdOVE9CXCh',\n    // 'spotify:track:1RdHfWJogQm1UW4MglA8gA',\n    // 'spotify:track:3z70bimZB3dgdixBrxpxY0',\n    // 'spotify:track:3RmCwMliRzxvjGp42ItZtC',\n    // 'spotify:track:6WpTrVTG1mFU1hZpxbVBX7',\n    // 'spotify:track:5sJiLlgQKBL81QCTOkoLB5',\n    // 'spotify:track:7hnqJYCKZFW7vMoykaraZG',\n\n    // Dance Punk\n    'spotify:track:305CEVdhAViS0CW2NCLvdR',\n    'spotify:track:1XlDNpWy8dyEljyRd0RC2J',\n    'spotify:track:1Jd9W7k8DTnBSovDSxK77n',\n    'spotify:track:7ddGC67DasWO30q5YepUJe',\n    'spotify:track:3yRV0V5l87Q6EyEnv3d7YJ',\n    'spotify:track:7pskYSHhRTH1TFtVdQevG5',\n    'spotify:track:3RCj5fG55qjtmnEML1gpnA',\n    'spotify:track:6b9oxWgxekphG5vkz8ZpBt',\n    'spotify:track:29wCKit7yf8ipSCViR7cGd',\n    'spotify:track:0Nua2OtL0ygR9HrY50ptQX',\n    'spotify:track:2Yx9fXTpx1cxL6m4cMq9AO',\n    'spotify:track:4d2sFYYGe1vQ65IXwm6mNt',\n  ],\n};\n\nfunction App() {\n  const URIsInput = useRef<HTMLInputElement>(null);\n  const isMounted = useRef(false);\n  const playerRef = useRef<HTMLDivElement>(null);\n\n  const code = new URLSearchParams(window.location.search).get('code');\n  const credentials = getCredentials();\n\n  const [\n    {\n      accessToken,\n      error,\n      hideAttribution,\n      inlineVolume,\n      isActive,\n      isPlaying,\n      layout,\n      refreshToken,\n      repeat,\n      shuffle,\n      styles,\n      transparent,\n      URIs,\n    },\n    setState,\n  ] = useSetState<State>({\n    accessToken: credentials.accessToken ?? '',\n    hideAttribution: false,\n    inlineVolume: true,\n    isActive: false,\n    isPlaying: false,\n    layout: 'responsive',\n    player: null,\n    refreshToken: credentials.refreshToken ?? '',\n    repeat: 'off',\n    shuffle: false,\n    styles: undefined,\n    transparent: false,\n    URIs: [baseURIs.artist],\n  });\n\n  useEffectOnce(() => {\n    if (code && !isMounted.current) {\n      login(code)\n        .then(spotifyCredentials => {\n          setCredentials(spotifyCredentials);\n          setState({\n            accessToken: spotifyCredentials.accessToken,\n            refreshToken: spotifyCredentials.refreshToken,\n          });\n        })\n        .catch(fetchError => {\n          setState({ error: fetchError.message || 'An error occurred. Try again' });\n        })\n        .finally(() => {\n          const url = new URL(window.location.href);\n\n          window.history.replaceState({}, document.title, `${url.pathname}`);\n        });\n    }\n\n    return () => {\n      isMounted.current = true;\n    };\n  });\n\n  useEffect(() => {\n    if (!playerRef.current) {\n      return;\n    }\n\n    playerRef.current.querySelector('a')?.setAttribute('draggable', `${layout === 'responsive'}`);\n    playerRef\n      .current.querySelector('img')\n      ?.setAttribute('draggable', `${layout === 'responsive'}`);\n\n    if (layout === 'responsive') {\n      playerRef.current.style.left = '0';\n      playerRef.current.style.right = '0';\n      playerRef.current.style.bottom = '0';\n      playerRef.current.style.top = 'auto';\n    } else {\n      playerRef.current.style.left = 'auto';\n      playerRef.current.style.right = '20px';\n      playerRef.current.style.bottom = '20px';\n      playerRef.current.style.top = 'auto';\n    }\n  }, [layout]);\n\n  useEffect(() => {\n    const dragOver = (event: DragEvent) => {\n      event.preventDefault();\n\n      if (!event.dataTransfer) {\n        return;\n      }\n\n      event.dataTransfer.dropEffect = 'move';\n    };\n\n    const drop = (event: DragEvent) => {\n      event.preventDefault();\n      const offsetData = event.dataTransfer?.getData('offset');\n\n      if (!offsetData) {\n        return;\n      }\n\n      const offset = JSON.parse(offsetData);\n      const xPos = event.clientX - offset.x;\n      const yPos = event.clientY - offset.y;\n\n      if (playerRef.current) {\n        playerRef.current.style.left = `${xPos}px`;\n        playerRef.current.style.top = `${yPos}px`;\n        playerRef.current.style.bottom = 'auto';\n        playerRef.current.style.right = 'auto';\n      }\n    };\n\n    document.documentElement.addEventListener('dragover', dragOver);\n    document.documentElement.addEventListener('drop', drop);\n\n    return () => {\n      document.documentElement.removeEventListener('dragover', dragOver);\n      document.documentElement.removeEventListener('drop', drop);\n    };\n  }, []);\n\n  const handleSubmitURIs = useCallback(\n    (event: FormEvent) => {\n      event.preventDefault();\n\n      if (URIsInput?.current) {\n        setState({ URIs: parseURIs(URIsInput.current.value) });\n      }\n    },\n    [setState],\n  );\n\n  const handleClickLogout = useCallback(() => {\n    logout();\n    setState({ accessToken: '', refreshToken: '' });\n  }, [setState]);\n\n  const handleClickURIs = useCallback(\n    (event: MouseEvent<HTMLButtonElement>) => {\n      event.preventDefault();\n      const { uris = '' } = event.currentTarget.dataset;\n\n      setState({ isPlaying: true, URIs: parseURIs(uris) });\n\n      if (URIsInput?.current) {\n        URIsInput.current.value = uris;\n      }\n    },\n    [setState],\n  );\n\n  const handleCallback = useCallback(\n    async ({ track, type, ...state }: CallbackState) => {\n      /* eslint-disable no-console */\n      console.group(`RSWP: ${type}`);\n      console.log(state);\n      console.groupEnd();\n      /* eslint-enable no-console */\n\n      if (type === TYPE.PLAYER) {\n        setState({\n          isActive: state.isActive,\n          isPlaying: state.isPlaying,\n          repeat: state.repeat,\n          shuffle: state.shuffle,\n        });\n      }\n\n      if (([TYPE.PRELOAD, TYPE.TRACK] as Array<Type>).includes(type)) {\n        const trackStyles = await request<StylesProps>(\n          `https://scripts.gilbarbara.dev/api/getImagePlayerStyles?url=${track.image}`,\n        );\n\n        if (transparent) {\n          trackStyles.bgColor = 'transparent';\n        }\n\n        setState({ styles: trackStyles });\n      }\n\n      if (state.status === STATUS.ERROR && state.errorType === ERROR_TYPE.AUTHENTICATION) {\n        refreshCredentials(refreshToken)\n          .then(spotifyCredentials => {\n            setCredentials(spotifyCredentials);\n            setState({ accessToken: spotifyCredentials.accessToken });\n          })\n          .catch(() => {\n            logout();\n            setState({ accessToken: '', refreshToken: '' });\n          });\n      }\n    },\n    [refreshToken, setState, transparent],\n  );\n\n  const handlePlayerDrag: DragEventHandler<HTMLDivElement> = useCallback(\n    event => {\n      if (layout === 'responsive') {\n        return;\n      }\n\n      const boundingRect = playerRef.current?.getBoundingClientRect() ?? { left: 0, top: 0 };\n      const offset = {\n        x: event.clientX - boundingRect.left,\n        y: event.clientY - boundingRect.top,\n      };\n\n      event.dataTransfer.setData('offset', JSON.stringify(offset));\n    },\n    [layout],\n  );\n\n  const getPlayer = useCallback(\n    async (playerInstance: SpotifyPlayer) => {\n      setState({ player: playerInstance });\n    },\n    [setState],\n  );\n\n  const content: Record<string, ReactNode> = {\n    connect: (\n      <Flex justify=\"center\" maxWidth={320} mx=\"auto\" width=\"100%\">\n        <Anchor href={getAuthorizeUrl()}>\n          <Button size=\"lg\">\n            <Icon mr=\"sm\" name=\"spotify\" size={24} />\n            <span>Connect</span>\n          </Button>\n        </Anchor>\n      </Flex>\n    ),\n  };\n\n  const getButtonStyle = (input: string) => {\n    return URIs.join(',') === input ? 'primary.300' : 'primary';\n  };\n\n  if (error) {\n    content.main = (\n      <>\n        <NonIdealState description={error} icon=\"close-o\" mb=\"lg\" title={null} />\n        {content.connect}\n      </>\n    );\n  } else if (code) {\n    content.main = <Loader size={200} />;\n  } else if (accessToken) {\n    content.main = (\n      <>\n        <Box as=\"form\" maxWidth={480} mx=\"auto\" onSubmit={handleSubmitURIs} width=\"100%\">\n          <FormElementWrapper\n            endContent={\n              <Button\n                shape=\"round\"\n                style={{ borderBottomLeftRadius: 0, borderTopLeftRadius: 0 }}\n                type=\"submit\"\n              >\n                <Icon name=\"check\" size={24} />\n              </Button>\n            }\n          >\n            <Input\n              ref={URIsInput}\n              data-flex={1}\n              defaultValue={URIs.join(',')}\n              name=\"uris\"\n              placeholder=\"Enter a Spotify URI\"\n              suffixSpacing={48}\n            />\n          </FormElementWrapper>\n        </Box>\n        <Flex basis=\"50%\" gap=\"md\" justify=\"center\" maxWidth={480} mt=\"xl\" mx=\"auto\" wrap=\"wrap\">\n          <Box textAlign=\"center\">\n            <Button\n              bg={getButtonStyle(baseURIs.artist)}\n              data-uris={baseURIs.artist}\n              onClick={handleClickURIs}\n              size=\"sm\"\n            >\n              Play an Artist\n            </Button>\n            <Paragraph mt=\"xxxs\">\n              <Anchor\n                color={getButtonStyle(baseURIs.artist)}\n                external\n                href=\"https://open.spotify.com/artist/7A0awCXkE1FtSU8B0qwOJQ?si=IP6f4hiVQ2Gk8XyepAhD0Q\"\n              >\n                Jamie xx\n              </Anchor>\n            </Paragraph>\n          </Box>\n          <Box textAlign=\"center\">\n            <Button\n              bg={getButtonStyle(baseURIs.album)}\n              data-uris={baseURIs.album}\n              onClick={handleClickURIs}\n              size=\"sm\"\n            >\n              Play an Album\n            </Button>\n            <Paragraph mt=\"xxxs\">\n              <Anchor\n                color={getButtonStyle(baseURIs.album)}\n                external\n                href=\"https://open.spotify.com/album/4c7fP0tUymaZcrEFIeIeZc?si=GarHO227QGuyfteTlpMSzA\"\n              >\n                Caribou - Honey\n              </Anchor>\n            </Paragraph>\n          </Box>\n          <Box textAlign=\"center\">\n            <Button\n              bg={getButtonStyle(baseURIs.playlist)}\n              data-uris={baseURIs.playlist}\n              onClick={handleClickURIs}\n              size=\"sm\"\n            >\n              Play a Playlist\n            </Button>\n            <Paragraph mt=\"xxxs\">\n              <Anchor\n                color={getButtonStyle(baseURIs.playlist)}\n                external\n                href=\"https://open.spotify.com/playlist/3h7lEfRkEdtVvGJTdTAudn?si=8294ef57c54d4fa8\"\n              >\n                Nation\n              </Anchor>\n            </Paragraph>\n          </Box>\n          <Box textAlign=\"center\">\n            <Button\n              bg={getButtonStyle(baseURIs.tracks.join(','))}\n              data-uris={baseURIs.tracks.join(',')}\n              onClick={handleClickURIs}\n              size=\"sm\"\n            >\n              Play some Tracks\n            </Button>\n            <Paragraph mt=\"xxxs\">Dance Punk</Paragraph>\n          </Box>\n          <Box textAlign=\"center\">\n            <Button\n              bg={getButtonStyle(baseURIs.show)}\n              data-uris={baseURIs.show}\n              onClick={handleClickURIs}\n              size=\"sm\"\n            >\n              Play a Show\n            </Button>\n            <Paragraph mt=\"xxxs\">\n              <Anchor\n                color={getButtonStyle(baseURIs.show)}\n                external\n                href=\"https://open.spotify.com/show/4kYCRYJ3yK5DQbP5tbfZby\"\n              >\n                Syntax\n              </Anchor>\n            </Paragraph>\n          </Box>\n        </Flex>\n        <Flex gap=\"xl\" justify=\"space-between\" maxWidth={400} mt=\"xl\" mx=\"auto\" width=\"100%\">\n          <Box>\n            <H4>Layout</H4>\n            <ButtonGroup\n              items={[{ label: 'responsive' }, { label: 'compact' }]}\n              onClick={event => setState({ layout: event.currentTarget.textContent as Layout })}\n              selected={layout}\n              size=\"sm\"\n            />\n          </Box>\n          <Box>\n            <H4>Props</H4>\n            <Flex direction=\"column\" gap=\"md\" mx=\"auto\" wrap=\"wrap\">\n              <Toggle\n                checked={hideAttribution}\n                label=\"Hide Attribution\"\n                name=\"hideAttribution\"\n                onToggle={() => setState({ hideAttribution: !hideAttribution })}\n              />\n              <Toggle\n                checked={inlineVolume}\n                label=\"Inline Volume\"\n                name=\"inlineVolume\"\n                onToggle={() => setState({ inlineVolume: !inlineVolume })}\n              />\n              <Toggle\n                checked={transparent}\n                label=\"Transparent\"\n                name=\"transparent\"\n                onToggle={value => setState({ transparent: value })}\n              />\n            </Flex>\n          </Box>\n        </Flex>\n      </>\n    );\n\n    content.player = (\n      <Player\n        key={accessToken}\n        ref={playerRef}\n        draggable={layout === 'compact'}\n        layout={layout}\n        onDragStart={handlePlayerDrag}\n      >\n        {layout === 'compact' && (\n          <ButtonUnstyled bg=\"white\" opacity={0.8} radius=\"xxs\">\n            <Icon name=\"maximize-alt\" size={20} />\n          </ButtonUnstyled>\n        )}\n        <SpotifyWebPlayer\n          callback={handleCallback}\n          components={{\n            leftButton: (\n              <ShuffleButton disabled={!isActive} shuffle={shuffle} token={accessToken} />\n            ),\n            rightButton: <RepeatButton disabled={!isActive} repeat={repeat} token={accessToken} />,\n          }}\n          getOAuthToken={async callback => {\n            if ((credentials.expiresAt ?? 0) < Math.round(Date.now() / 1000)) {\n              const newCredentials = await refreshCredentials(refreshToken);\n\n              setCredentials(newCredentials);\n              setState({\n                accessToken: newCredentials.accessToken,\n                refreshToken: newCredentials.refreshToken,\n              });\n\n              callback(newCredentials.accessToken);\n            } else {\n              callback(accessToken);\n            }\n          }}\n          getPlayer={getPlayer}\n          hideAttribution={hideAttribution}\n          initialVolume={100}\n          inlineVolume={inlineVolume}\n          layout={layout}\n          persistDeviceSelection\n          play={isPlaying}\n          preloadData\n          showSaveIcon\n          styles={transparent ? { ...styles, bgColor: 'transparent' } : styles}\n          syncExternalDevice\n          token={accessToken}\n          uris={URIs}\n        />\n      </Player>\n    );\n  } else {\n    content.main = content.connect;\n  }\n\n  return (\n    <>\n      <GlobalStyles hasToken={!!accessToken} />\n      <Container fullScreen fullScreenOffset={accessToken ? 100 : 0} justify=\"center\">\n        <Spacer distribution=\"center\" mb=\"xl\">\n          <H1 align=\"center\" mb={0}>\n            React Spotify Web Playback\n          </H1>\n\n          {accessToken && (\n            <Button onClick={handleClickLogout} size=\"xs\">\n              <Icon name=\"sign-out\" size={14} />\n            </Button>\n          )}\n        </Spacer>\n\n        {content.main}\n        {content.player}\n      </Container>\n    </>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "demo/src/GitHubRepo.tsx",
    "content": "import styled from '@emotion/styled';\n\nimport { primaryColor } from './modules/theme';\n\nconst Wrapper = styled.a`\n  position: fixed;\n  top: 0;\n  right: 0;\n\n  &:hover {\n    .octo-arm {\n      animation: octocat-wave 560ms ease-in-out;\n    }\n  }\n\n  svg {\n    fill: ${primaryColor};\n    color: #fff;\n  }\n\n  .octo-arm {\n    transform-origin: 130px 106px;\n  }\n\n  @keyframes octocat-wave {\n    0%,\n    100% {\n      transform: rotate(0);\n    }\n    20%,\n    60% {\n      transform: rotate(-25deg);\n    }\n    40%,\n    80% {\n      transform: rotate(10deg);\n    }\n  }\n`;\n\nfunction GitHubRepo() {\n  return (\n    <Wrapper\n      aria-label=\"View source on GitHub\"\n      className=\"github-corner\"\n      href=\"https://github.com/gilbarbara/react-spotify-web-playback\"\n      rel=\"noopener noreferrer\"\n      target=\"_blank\"\n    >\n      <svg aria-hidden=\"true\" height=\"80\" viewBox=\"0 0 250 250\" width=\"80\">\n        <path d=\"M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z\" />\n        <path\n          className=\"octo-arm\"\n          d=\"M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2\"\n          fill=\"currentColor\"\n        />\n        <path\n          className=\"octo-body\"\n          d=\"M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    </Wrapper>\n  );\n}\n\nexport default GitHubRepo;\n"
  },
  {
    "path": "demo/src/components/GlobalStyles.tsx",
    "content": "import { css, Global } from '@emotion/react';\nimport { theme } from '@gilbarbara/components';\n\n// background: linear-gradient(\n//   0deg,\n//   oklch(0.65 0.3 29.62 / 0.8),\n// oklch(0.65 0.3 29.62 / 0) 75%\n// ),\n// linear-gradient(60deg, oklch(0.96 0.25 110.23 / 0.8), oklch(0.96 0.25 110.23 / 0) 75%),\n// linear-gradient(120deg, oklch(0.85 0.36 144.24 / 0.8), oklch(0.85 0.36 144.24 / 0) 75%),\n// linear-gradient(180deg, oklch(0.89 0.2 194.59 / 0.8), oklch(0.89 0.2 194.18 / 0) 75%),\n// linear-gradient(240deg, oklch(0.47 0.32 264.05 / 0.8), oklch(0.47 0.32 264.05 / 0) 75%),\n// linear-gradient(300deg, oklch(0.7 0.35 327.92 / 0.8), oklch(0.7 0.35 327.92 / 0) 75%);\n\nexport default function GlobalStyles({ hasToken }: any) {\n  return (\n    <Global\n      styles={css`\n        body {\n          background-color: #001638;\n          color: ${theme.lightColor};\n          box-sizing: border-box;\n          font-family: sans-serif;\n          margin: 0;\n          min-height: 100vh;\n          padding: 0 0 ${hasToken ? '100px' : 0};\n        }\n\n        .github-corner {\n          position: fixed;\n          top: 0;\n          right: 0;\n        }\n\n        .github-corner svg {\n          fill: ${theme.colors.primary};\n          color: #fff;\n        }\n\n        .github-corner .octo-arm {\n          transform-origin: 130px 106px;\n        }\n\n        .github-corner:hover .octo-arm {\n          animation: octocat-wave 560ms ease-in-out;\n        }\n\n        @keyframes octocat-wave {\n          0%,\n          100% {\n            transform: rotate(0);\n          }\n          20%,\n          60% {\n            transform: rotate(-25deg);\n          }\n          40%,\n          80% {\n            transform: rotate(10deg);\n          }\n        }\n\n        @media (max-width: 500px) {\n          .github-corner:hover .octo-arm {\n            animation: none;\n          }\n\n          .github-corner .octo-arm {\n            animation: octocat-wave 560ms ease-in-out;\n          }\n        }\n      `}\n    />\n  );\n}\n"
  },
  {
    "path": "demo/src/components/Player.tsx",
    "content": "import { Layout } from 'react-spotify-web-playback';\nimport { css } from '@emotion/react';\nimport styled from '@emotion/styled';\n\nconst Player = styled.div<{ layout: Layout }>(({ layout }) => {\n  if (layout === 'responsive') {\n    return css`\n      position: fixed;\n      bottom: 0;\n      left: 0;\n      right: 0;\n    `;\n  }\n\n  return css`\n    border-radius: 12px;\n    box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);\n    bottom: 20px;\n    overflow: hidden;\n    position: fixed;\n    right: 20px;\n    width: 320px;\n\n    > button {\n      position: absolute;\n      top: 8px;\n      right: 8px;\n      z-index: 100;\n    }\n  `;\n});\n\nexport default Player;\n"
  },
  {
    "path": "demo/src/components/RepeatButton.tsx",
    "content": "import { ComponentProps, useCallback } from 'react';\nimport { RepeatState, spotifyApi } from 'react-spotify-web-playback';\nimport { ButtonUnstyled, FlexInline, Icon } from '@gilbarbara/components';\n\nexport default function RepeatButton({\n  repeat,\n  token,\n  ...rest\n}: Omit<ComponentProps<typeof ButtonUnstyled>, 'children'> & {\n  repeat: RepeatState;\n  token: string;\n}) {\n  const handleClick = useCallback(async () => {\n    let value: RepeatState = 'off';\n\n    if (repeat === 'off') {\n      value = 'track';\n    } else if (repeat === 'context') {\n      value = 'track';\n    }\n\n    await spotifyApi.repeat(token, value);\n  }, [repeat, token]);\n\n  let title = 'Enable repeat';\n\n  if (repeat === 'track') {\n    title = 'Disable repeat';\n  }\n\n  if (repeat === 'context') {\n    title = 'Enable repeat one';\n  }\n\n  return (\n    <ButtonUnstyled\n      height={32}\n      justify=\"center\"\n      onClick={handleClick}\n      title={title}\n      width={32}\n      {...rest}\n    >\n      <FlexInline position=\"relative\">\n        <Icon name=\"repeat\" size={24} title={null} />\n        {repeat !== 'off' && (\n          <span\n            style={{\n              fontSize: 9,\n              fontWeight: 700,\n              position: 'absolute',\n              top: 5,\n              left: '50%',\n              transform: 'translateX(-50%)',\n            }}\n          >\n            {repeat === 'track' ? '1' : 'all'}\n          </span>\n        )}\n      </FlexInline>\n    </ButtonUnstyled>\n  );\n}\n"
  },
  {
    "path": "demo/src/components/ShuffleButton.tsx",
    "content": "import { ComponentProps, useCallback } from 'react';\nimport { spotifyApi } from 'react-spotify-web-playback';\nimport { ButtonUnstyled, FlexInline, Icon } from '@gilbarbara/components';\n\nexport default function ShuffleButton({\n  shuffle,\n  token,\n  ...rest\n}: Omit<ComponentProps<typeof ButtonUnstyled>, 'children'> & {\n  shuffle: boolean;\n  token: string;\n}) {\n  const handleClick = useCallback(async () => {\n    await spotifyApi.shuffle(token, !shuffle);\n  }, [shuffle, token]);\n\n  return (\n    <ButtonUnstyled\n      height={32}\n      justify=\"center\"\n      onClick={handleClick}\n      title={`${shuffle ? 'Disable' : 'Enable'} repeat`}\n      width={32}\n      {...rest}\n    >\n      <FlexInline position=\"relative\">\n        <Icon name=\"shuffle\" size={20} title={null} />\n        {shuffle && (\n          <span\n            style={{\n              fontSize: 8,\n              fontWeight: 700,\n              position: 'absolute',\n              top: '50%',\n              left: -2,\n              transform: 'translateY(-50%)',\n            }}\n          >\n            ⏺\n          </span>\n        )}\n      </FlexInline>\n    </ButtonUnstyled>\n  );\n}\n"
  },
  {
    "path": "demo/src/index.tsx",
    "content": "import { ThemeProvider } from '@emotion/react';\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport App from './App';\nimport { theme } from './modules/theme';\n\nconst rootElement = document.getElementById('root');\n\nif (rootElement) {\n  const root = createRoot(rootElement);\n\n  root.render(\n    <StrictMode>\n      <ThemeProvider theme={theme}>\n        <App />\n      </ThemeProvider>\n    </StrictMode>,\n  );\n}\n"
  },
  {
    "path": "demo/src/modules/helpers.ts",
    "content": "import { getCookie, removeCookie, setCookie } from '@gilbarbara/cookies';\nimport { MONTH, request } from '@gilbarbara/helpers';\n\nimport { SpotifyCredentials } from '../types';\n\nconst COOKIE_NAME = 'RSWP_TOKENS';\n\nconst { NODE_ENV } = process.env;\n\nexport const SPOTIFY = {\n  accountApiUrl: 'https://accounts.spotify.com',\n  clientId: '2030beede5174f9f9b23ffc23ba0705c',\n  redirectUri:\n    NODE_ENV === 'production'\n      ? 'https://react-spotify-web-playback.gilbarbara.dev'\n      : 'http://localhost:3000',\n  scopes: [\n    'streaming',\n    'user-read-email',\n    'user-read-private',\n    'user-library-read',\n    'user-library-modify',\n    'user-read-playback-state',\n    'user-modify-playback-state',\n  ],\n};\nexport const API_URL = 'https://scripts.gilbarbara.dev/api';\n\nexport function getAuthorizeUrl() {\n  const parameters = {\n    client_id: SPOTIFY.clientId,\n    response_type: 'code',\n    redirect_uri: SPOTIFY.redirectUri,\n    scope: SPOTIFY.scopes.join(' '),\n    state: 'auth',\n  };\n\n  return `${SPOTIFY.accountApiUrl}/authorize?${new URLSearchParams(parameters)}`;\n}\n\nexport function getCredentials() {\n  const tokens = getCookie(COOKIE_NAME);\n\n  if (tokens) {\n    return JSON.parse(tokens) as SpotifyCredentials;\n  }\n\n  return {} as Partial<SpotifyCredentials>;\n}\n\nexport function setCredentials(credentials: SpotifyCredentials) {\n  setCookie(COOKIE_NAME, JSON.stringify(credentials), { expires: MONTH * 6 });\n}\n\nexport async function login(code: string) {\n  return request<SpotifyCredentials>(`${API_URL}/spotifyGetUserCredentials`, {\n    method: 'POST',\n    body: { code, redirectUri: SPOTIFY.redirectUri },\n  });\n}\n\nexport function refreshCredentials(refreshToken: string) {\n  return request<SpotifyCredentials>(`${API_URL}/spotifyRefreshToken`, {\n    method: 'POST',\n    body: { refreshToken },\n  });\n}\n\nexport function logout() {\n  removeCookie(COOKIE_NAME);\n}\n\nexport function parseURIs(input: string): string[] {\n  const ids = input.split(',');\n\n  return ids.every(d => validateURI(d)) ? ids : [];\n}\n\nexport function validateURI(input: string): boolean {\n  let isValid = false;\n\n  if (input?.includes(':')) {\n    const [key, type, id] = input.split(':');\n\n    if (key && type && type !== 'user' && id && id.length === 22) {\n      isValid = true;\n    }\n  }\n\n  return isValid;\n}\n"
  },
  {
    "path": "demo/src/modules/theme.ts",
    "content": "import { mergeTheme } from '@gilbarbara/components';\n\nexport const primaryColor = '#ff6d57';\n\nexport const theme = mergeTheme({\n  darkMode: true,\n  colors: {\n    primary: primaryColor,\n  },\n});\n"
  },
  {
    "path": "demo/src/types.ts",
    "content": "export interface SpotifyCredentials {\n  accessToken: string;\n  expiresAt: number;\n  refreshToken?: string;\n  scope: string[];\n}\n"
  },
  {
    "path": "demo/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"target\": \"es5\"\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"react-spotify-web-playback\",\n  \"version\": \"0.14.7\",\n  \"description\": \"A React Spotify Web Player\",\n  \"author\": \"Gil Barbara <gilbarbara@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/gilbarbara/react-spotify-web-playback.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gilbarbara/react-spotify-web-playback/issues\"\n  },\n  \"homepage\": \"https://github.com/gilbarbara/react-spotify-web-playback#readme\",\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \"import\": \"./dist/index.mjs\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"types\": \"dist/index.d.ts\",\n  \"sideEffects\": true,\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"react\",\n    \"react-component\",\n    \"spotify\",\n    \"player\",\n    \"web playback\"\n  ],\n  \"peerDependencies\": {\n    \"react\": \"17 - 19\"\n  },\n  \"dependencies\": {\n    \"@gilbarbara/deep-equal\": \"^0.3.1\",\n    \"@gilbarbara/react-range-slider\": \"^0.7.0\",\n    \"@types/spotify-api\": \"^0.0.25\",\n    \"@types/spotify-web-playback-sdk\": \"^0.1.19\",\n    \"colorizr\": \"^3.0.7\",\n    \"memoize-one\": \"^6.0.0\",\n    \"nano-css\": \"^5.6.2\"\n  },\n  \"devDependencies\": {\n    \"@arethetypeswrong/cli\": \"^0.17.3\",\n    \"@gilbarbara/eslint-config\": \"^0.8.4\",\n    \"@gilbarbara/hooks\": \"^0.9.0\",\n    \"@gilbarbara/prettier-config\": \"^1.0.0\",\n    \"@gilbarbara/tsconfig\": \"^0.2.3\",\n    \"@size-limit/file\": \"^11.1.6\",\n    \"@swc/core\": \"^1.10.7\",\n    \"@testing-library/dom\": \"^10.4.0\",\n    \"@testing-library/jest-dom\": \"^6.6.3\",\n    \"@testing-library/react\": \"^16.1.0\",\n    \"@types/exenv\": \"^1.2.2\",\n    \"@types/node\": \"^22.10.6\",\n    \"@types/once\": \"^1.4.5\",\n    \"@types/react\": \"^18.3.12\",\n    \"@types/react-dom\": \"^18.3.1\",\n    \"@vitejs/plugin-react-swc\": \"^3.7.2\",\n    \"@vitest/coverage-v8\": \"^2.1.8\",\n    \"del-cli\": \"^6.0.0\",\n    \"fix-tsup-cjs\": \"^1.2.0\",\n    \"husky\": \"^9.1.7\",\n    \"is-ci-cli\": \"^2.2.0\",\n    \"jest-extended\": \"^4.0.2\",\n    \"jsdom\": \"^26.0.0\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"repo-tools\": \"^0.3.1\",\n    \"size-limit\": \"^11.1.6\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsup\": \"^8.3.5\",\n    \"typescript\": \"^5.7.3\",\n    \"vite-tsconfig-paths\": \"^5.1.4\",\n    \"vitest\": \"^2.1.8\",\n    \"vitest-fetch-mock\": \"^0.4.3\"\n  },\n  \"scripts\": {\n    \"build\": \"npm run clean && tsup && fix-tsup-cjs\",\n    \"clean\": \"del dist/*\",\n    \"watch\": \"tsup --watch\",\n    \"lint\": \"eslint --fix src test\",\n    \"test\": \"is-ci \\\"test:coverage\\\" \\\"test:watch\\\"\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"test:watch\": \"vitest watch\",\n    \"typecheck\": \"tsc -p test/tsconfig.json\",\n    \"typevalidation\": \"attw -P\",\n    \"format\": \"prettier \\\"**/*.{js,jsx,json,yml,yaml,css,less,scss,ts,tsx,md,graphql,mdx}\\\" --write\",\n    \"validate\": \"npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run typevalidation && npm run size\",\n    \"size\": \"size-limit\",\n    \"prepublishOnly\": \"npm run validate\",\n    \"prepare\": \"husky\"\n  },\n  \"tsup\": {\n    \"dts\": true,\n    \"entry\": [\n      \"src/index.tsx\"\n    ],\n    \"format\": [\n      \"cjs\",\n      \"esm\"\n    ],\n    \"sourcemap\": true,\n    \"splitting\": false\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"@gilbarbara/eslint-config\",\n      \"@gilbarbara/eslint-config/vitest\",\n      \"@gilbarbara/eslint-config/testing-library\"\n    ],\n    \"overrides\": [\n      {\n        \"files\": [\n          \"test/**/*.ts?(x)\"\n        ],\n        \"rules\": {\n          \"no-console\": \"off\"\n        }\n      }\n    ],\n    \"rules\": {\n      \"@typescript-eslint/no-non-null-assertion\": \"off\",\n      \"react/sort-comp\": \"off\",\n      \"unicorn/prefer-includes\": \"off\"\n    }\n  },\n  \"eslintIgnore\": [\n    \"demo\"\n  ],\n  \"prettier\": \"@gilbarbara/prettier-config\",\n  \"size-limit\": [\n    {\n      \"name\": \"commonjs\",\n      \"path\": \"./dist/index.js\",\n      \"limit\": \"20 KB\"\n    },\n    {\n      \"name\": \"esm\",\n      \"path\": \"./dist/index.mjs\",\n      \"limit\": \"20 KB\"\n    }\n  ]\n}\n"
  },
  {
    "path": "sonar-project.properties",
    "content": "sonar.projectKey=gilbarbara_react-spotify-web-playback\nsonar.organization=gilbarbara-github\nsonar.source=./src\nsonar.javascript.lcov.reportPaths=./coverage/lcov.info\nsonar.exclusions=**/demo/**/*.*,**/test/**/*.*\nsonar.coverage.exclusions=**/test/**/*.*,**/vitest.config.mts\n"
  },
  {
    "path": "src/components/Actions.tsx",
    "content": "import { memo, ReactNode } from 'react';\n\nimport { CssLikeObject, px, styled } from '~/modules/styled';\n\nimport { Layout, StyledProps, StylesOptions } from '~/types';\n\ninterface Props {\n  children: ReactNode;\n  layout: Layout;\n  styles: StylesOptions;\n}\n\nconst Wrapper = styled('div')(\n  {\n    alignItems: 'center',\n    display: 'flex',\n    justifyContent: 'flex-end',\n    'pointer-events': 'none',\n  },\n  ({ style }: StyledProps) => {\n    let styles: CssLikeObject = {\n      bottom: 0,\n      position: 'absolute',\n      right: 0,\n      width: 'auto',\n    };\n\n    if (style.layout === 'responsive') {\n      styles = {\n        '@media (max-width: 767px)': styles,\n        '@media (min-width: 768px)': {\n          height: px(style.h),\n        },\n      };\n    }\n\n    return {\n      height: px(32),\n      ...styles,\n    };\n  },\n  'ActionsRSWP',\n);\n\nfunction Actions(props: Props) {\n  const { children, layout, styles } = props;\n\n  return (\n    <Wrapper data-component-name=\"Actions\" style={{ h: styles.height, layout }}>\n      {children}\n    </Wrapper>\n  );\n}\n\nexport default memo(Actions);\n"
  },
  {
    "path": "src/components/ClickOutside.tsx",
    "content": "import { memo, ReactNode, useEffect, useRef } from 'react';\n\ninterface Props {\n  children: ReactNode;\n  isActive: boolean;\n  onClick: () => void;\n}\n\nfunction ClickOutside(props: Props) {\n  const { children, isActive, onClick, ...rest } = props;\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const isTouch = useRef(false);\n\n  const handleClick = useRef((event: MouseEvent | TouchEvent) => {\n    const container = containerRef.current;\n\n    if (event.type === 'touchend') {\n      isTouch.current = true;\n    }\n\n    if (event.type === 'click' && isTouch.current) {\n      return;\n    }\n\n    if (container && !container.contains(event.target as Node)) {\n      onClick();\n    }\n  });\n\n  useEffect(() => {\n    const { current } = handleClick;\n\n    if (isActive) {\n      document.addEventListener('touchend', current, true);\n      document.addEventListener('click', current, true);\n    }\n\n    return () => {\n      document.removeEventListener('touchend', current, true);\n      document.removeEventListener('click', current, true);\n    };\n  }, [isActive]);\n\n  return (\n    <div ref={containerRef} {...rest}>\n      {children}\n    </div>\n  );\n}\n\nexport default memo(ClickOutside);\n"
  },
  {
    "path": "src/components/Controls.tsx",
    "content": "import { memo } from 'react';\n\nimport { CssLikeObject, px, styled } from '~/modules/styled';\n\nimport {\n  CustomComponents,\n  Layout,\n  Locale,\n  SpotifyTrack,\n  StyledProps,\n  StylesOptions,\n} from '~/types';\n\nimport Next from './icons/Next';\nimport Pause from './icons/Pause';\nimport Play from './icons/Play';\nimport Previous from './icons/Previous';\nimport Slider from './Slider';\n\ninterface Props {\n  components?: CustomComponents;\n  devices: JSX.Element | null;\n  durationMs: number;\n  isActive: boolean;\n  isExternalDevice: boolean;\n  isMagnified: boolean;\n  isPlaying: boolean;\n  layout: Layout;\n  locale: Locale;\n  nextTracks: SpotifyTrack[];\n  onChangeRange: (position: number) => void;\n  onClickNext: () => void;\n  onClickPrevious: () => void;\n  onClickTogglePlay: () => void;\n  onToggleMagnify: () => void;\n  position: number;\n  progressMs: number;\n  styles: StylesOptions;\n  volume: JSX.Element | null;\n}\n\nconst Wrapper = styled('div')(\n  {\n    '.rswp__volume': {\n      position: 'absolute',\n      right: 0,\n      top: 0,\n    },\n    '.rswp__devices': {\n      position: 'absolute',\n      left: 0,\n      top: 0,\n    },\n  },\n  ({ style }: StyledProps) => {\n    const isCompactLayout = style.layout === 'compact';\n    const styles: CssLikeObject = {};\n\n    if (isCompactLayout) {\n      styles.padding = px(8);\n    } else {\n      styles.padding = `${px(4)} 0`;\n      styles['@media (max-width: 767px)'] = {\n        padding: px(8),\n      };\n    }\n\n    return styles;\n  },\n  'ControlsRSWP',\n);\n\nconst Buttons = styled('div')(\n  {\n    alignItems: 'center',\n    display: 'flex',\n    justifyContent: 'center',\n    marginBottom: px(8),\n    position: 'relative',\n\n    '> div': {\n      alignItems: 'center',\n      display: 'flex',\n      minWidth: px(32),\n      textAlign: 'center',\n    },\n  },\n  ({ style }: StyledProps) => ({\n    color: style.c,\n  }),\n  'ControlsButtonsRSWP',\n);\n\nconst Button = styled('button')(\n  {\n    alignItems: 'center',\n    display: 'inline-flex',\n    fontSize: px(16),\n    height: px(32),\n    justifyContent: 'center',\n    width: px(32),\n\n    '&:disabled': {\n      cursor: 'default',\n      opacity: 0.6,\n    },\n\n    '&.rswp__toggle': {\n      fontSize: px(32),\n      width: px(48),\n    },\n  },\n  () => ({}),\n  'ControlsButtonRSWP',\n);\n\nfunction Controls(props: Props) {\n  const {\n    components: { leftButton, rightButton } = {},\n    devices,\n    durationMs,\n    isActive,\n    isExternalDevice,\n    isMagnified,\n    isPlaying,\n    layout,\n    locale,\n    nextTracks,\n    onChangeRange,\n    onClickNext,\n    onClickPrevious,\n    onClickTogglePlay,\n    onToggleMagnify,\n    position,\n    progressMs,\n    styles,\n    volume,\n  } = props;\n\n  const { color } = styles;\n\n  return (\n    <Wrapper data-component-name=\"Controls\" data-playing={isPlaying} style={{ layout }}>\n      <Buttons style={{ c: color }}>\n        {devices && <div className=\"rswp__devices\">{devices}</div>}\n        <div>{leftButton}</div>\n        <div>\n          <Button\n            aria-label={locale.previous}\n            className=\"ButtonRSWP\"\n            disabled={!isActive && !isExternalDevice}\n            onClick={onClickPrevious}\n            title={locale.previous}\n            type=\"button\"\n          >\n            <Previous />\n          </Button>\n        </div>\n        <div>\n          <Button\n            aria-label={isPlaying ? locale.pause : locale.play}\n            className=\"ButtonRSWP rswp__toggle\"\n            onClick={onClickTogglePlay}\n            title={isPlaying ? locale.pause : locale.play}\n            type=\"button\"\n          >\n            {isPlaying ? <Pause /> : <Play />}\n          </Button>\n        </div>\n        <div>\n          <Button\n            aria-label={locale.next}\n            className=\"ButtonRSWP\"\n            disabled={!nextTracks.length && !isActive && !isExternalDevice}\n            onClick={onClickNext}\n            title={locale.next}\n            type=\"button\"\n          >\n            <Next />\n          </Button>\n        </div>\n        <div>{rightButton}</div>\n        {volume && <div className=\"rswp__volume\">{volume}</div>}\n      </Buttons>\n      <Slider\n        durationMs={durationMs}\n        isMagnified={isMagnified}\n        onChangeRange={onChangeRange}\n        onToggleMagnify={onToggleMagnify}\n        position={position}\n        progressMs={progressMs}\n        styles={styles}\n      />\n    </Wrapper>\n  );\n}\n\nexport default memo(Controls);\n"
  },
  {
    "path": "src/components/Devices.tsx",
    "content": "import { MouseEvent, useCallback, useState } from 'react';\nimport { CssLikeObject } from 'nano-css';\n\nimport { px, styled } from '~/modules/styled';\n\nimport { Layout, Locale, SpotifyDevice, StyledProps, StylesOptions } from '~/types';\n\nimport ClickOutside from './ClickOutside';\nimport DevicesIcon from './icons/Devices';\nimport DevicesComputerIcon from './icons/DevicesComputer';\nimport DevicesMobileIcon from './icons/DevicesMobile';\nimport DevicesSpeakerIcon from './icons/DevicesSpeaker';\n\ninterface DeviceList {\n  currentDevice: SpotifyDevice | null;\n  otherDevices: SpotifyDevice[];\n}\n\ninterface Props {\n  currentDeviceId?: string;\n  deviceId?: string;\n  devices: SpotifyDevice[];\n  layout: Layout;\n  locale: Locale;\n  onClickDevice: (deviceId: string) => any;\n  open: boolean;\n  playerPosition: string;\n  styles: StylesOptions;\n}\n\nconst Wrapper = styled('div')(\n  {\n    'pointer-events': 'all',\n    alignItems: 'center',\n    display: 'flex',\n    justifyContent: 'center',\n    position: 'relative',\n    zIndex: 20,\n\n    '> div': {\n      backgroundColor: '#000',\n      borderRadius: px(8),\n      color: '#fff',\n      filter: 'drop-shadow(1px 1px 6px rgba(0, 0, 0, 0.5))',\n      fontSize: px(14),\n      padding: px(16),\n      position: 'absolute',\n      textAlign: 'left',\n\n      '> p': {\n        fontWeight: 'bold',\n        marginBottom: px(8),\n        marginTop: px(16),\n        whiteSpace: 'nowrap',\n      },\n\n      button: {\n        alignItems: 'center',\n        display: 'flex',\n        whiteSpace: 'nowrap',\n        width: '100%',\n\n        '&:not(:last-of-type)': {\n          marginBottom: px(12),\n        },\n\n        span: {\n          display: 'inline-block',\n          marginLeft: px(4),\n        },\n      },\n\n      '> span': {\n        background: 'transparent',\n        borderLeft: `6px solid transparent`,\n        borderRight: `6px solid transparent`,\n        content: '\"\"',\n        display: 'block',\n        height: 0,\n        position: 'absolute',\n        width: 0,\n      },\n    },\n\n    '> button': {\n      alignItems: 'center',\n      display: 'flex',\n      fontSize: px(24),\n      height: px(32),\n      justifyContent: 'center',\n      width: px(32),\n    },\n  },\n  ({ style }: StyledProps) => {\n    const isCompact = style.layout === 'compact';\n    const divStyles: CssLikeObject = isCompact\n      ? {\n          bottom: '120%',\n          left: 0,\n        }\n      : {\n          [style.p]: '120%',\n          left: 0,\n\n          '@media (min-width: 768px)': {\n            left: 'auto',\n            right: 0,\n          },\n        };\n    const spanStyles: CssLikeObject = isCompact\n      ? {\n          bottom: `-${px(6)}`,\n          borderTop: `6px solid #000`,\n          left: px(10),\n        }\n      : {\n          [style.p === 'top' ? 'border-bottom' : 'border-top']: `6px solid #000`,\n          [style.p]: '-6px',\n          left: px(10),\n\n          '@media (min-width: 768px)': {\n            left: 'auto',\n            right: px(10),\n          },\n        };\n\n    return {\n      '> button': {\n        color: style.c,\n      },\n\n      '> div': {\n        ...divStyles,\n\n        '> span': spanStyles,\n      },\n    };\n  },\n  'DevicesRSWP',\n);\n\nconst ListHeader = styled('div')({\n  p: {\n    whiteSpace: 'nowrap',\n\n    '&:nth-of-type(1)': {\n      fontWeight: 'bold',\n      marginBottom: px(8),\n    },\n\n    '&:nth-of-type(2)': {\n      alignItems: 'center',\n      display: 'flex',\n\n      span: {\n        display: 'inline-block',\n        marginLeft: px(4),\n      },\n    },\n  },\n});\n\nfunction getDeviceIcon(type: string) {\n  if (type.toLowerCase().includes('speaker')) {\n    return <DevicesSpeakerIcon />;\n  }\n\n  if (type.toLowerCase().includes('computer')) {\n    return <DevicesComputerIcon />;\n  }\n\n  return <DevicesMobileIcon />;\n}\n\nexport default function Devices(props: Props) {\n  const {\n    currentDeviceId,\n    deviceId,\n    devices = [],\n    layout,\n    locale,\n    onClickDevice,\n    open,\n    playerPosition,\n    styles: { color },\n  } = props;\n  const [isOpen, setOpen] = useState(open);\n\n  const handleClickSetDevice = (event: MouseEvent<HTMLElement>) => {\n    const { dataset } = event.currentTarget;\n\n    if (dataset.id) {\n      onClickDevice(dataset.id);\n\n      setOpen(false);\n    }\n  };\n\n  const handleClickToggleList = useCallback(() => {\n    setOpen(s => !s);\n  }, []);\n\n  const { currentDevice, otherDevices } = devices.reduce<DeviceList>(\n    (acc, device) => {\n      if (device.id === currentDeviceId) {\n        acc.currentDevice = device;\n      } else {\n        acc.otherDevices.push(device);\n      }\n\n      return acc;\n    },\n    { currentDevice: null, otherDevices: [] },\n  );\n\n  let icon = <DevicesIcon />;\n\n  if (deviceId && currentDevice && currentDevice.id !== deviceId) {\n    icon = getDeviceIcon(currentDevice.type);\n  }\n\n  return (\n    <ClickOutside isActive={isOpen} onClick={handleClickToggleList}>\n      <Wrapper\n        data-component-name=\"Devices\"\n        data-device-id={currentDeviceId}\n        style={{\n          c: color,\n          layout,\n          p: playerPosition,\n        }}\n      >\n        {!!devices.length && (\n          <>\n            {isOpen && (\n              <div>\n                {currentDevice && (\n                  <ListHeader>\n                    <p>{locale.currentDevice}</p>\n                    <p>\n                      {getDeviceIcon(currentDevice.type)}\n                      <span>{currentDevice.name}</span>\n                    </p>\n                  </ListHeader>\n                )}\n                {!!otherDevices.length && (\n                  <>\n                    <p>{locale.otherDevices}</p>\n                    {otherDevices.map(device => (\n                      <button\n                        key={device.id}\n                        aria-label={device.name}\n                        className=\"ButtonRSWP\"\n                        data-id={device.id}\n                        onClick={handleClickSetDevice}\n                        type=\"button\"\n                      >\n                        {getDeviceIcon(device.type)}\n                        <span>{device.name}</span>\n                      </button>\n                    ))}\n                  </>\n                )}\n                <span />\n              </div>\n            )}\n            <button\n              aria-label={locale.devices}\n              className=\"ButtonRSWP\"\n              onClick={handleClickToggleList}\n              title={locale.devices}\n              type=\"button\"\n            >\n              {icon}\n            </button>\n          </>\n        )}\n      </Wrapper>\n    </ClickOutside>\n  );\n}\n"
  },
  {
    "path": "src/components/ErrorMessage.tsx",
    "content": "import { px, styled } from '~/modules/styled';\n\nimport { ComponentsProps, StyledProps } from '~/types';\n\nconst Wrapper = styled('div')(\n  {\n    alignItems: 'center',\n    display: 'flex',\n    justifyContent: 'center',\n    textAlign: 'center',\n    width: '100%',\n  },\n  ({ style }: StyledProps) => ({\n    backgroundColor: style.bgColor,\n    borderTop: `1px solid ${style.errorColor}`,\n    color: style.errorColor,\n    height: px(style.h),\n  }),\n  'ErrorRSWP',\n);\n\nexport default function ErrorMessage({\n  children,\n  styles: { bgColor, errorColor, height },\n}: ComponentsProps) {\n  return (\n    <Wrapper data-component-name=\"ErrorMessage\" style={{ bgColor, errorColor, h: height }}>\n      {children}\n    </Wrapper>\n  );\n}\n"
  },
  {
    "path": "src/components/Info.tsx",
    "content": "import { memo, ReactNode, useEffect, useRef, useState } from 'react';\nimport { opacify } from 'colorizr';\n\nimport { getBgColor, getSpotifyLink, getSpotifyLinkTitle } from '~/modules/getters';\nimport { usePrevious } from '~/modules/hooks';\nimport { checkTracksStatus, removeTracks, saveTracks } from '~/modules/spotify';\nimport { CssLikeObject, px, styled } from '~/modules/styled';\n\nimport { Layout, Locale, SpotifyTrack, StyledProps, StylesOptions } from '~/types';\n\nimport Favorite from './icons/Favorite';\nimport FavoriteOutline from './icons/FavoriteOutline';\nimport SpotifyLogo from './SpotifyLogo';\n\ninterface Props {\n  hideAttribution: boolean;\n  hideCoverArt: boolean;\n  isActive: boolean;\n  layout: Layout;\n  locale: Locale;\n  onFavoriteStatusChange: (status: boolean) => any;\n  showSaveIcon: boolean;\n  styles: StylesOptions;\n  token: string;\n  track: SpotifyTrack;\n  updateSavedStatus?: (fn: (status: boolean) => any) => any;\n}\n\nconst imageSize = 64;\nconst iconSize = 32;\n\nconst Wrapper = styled('div')(\n  {\n    textAlign: 'left',\n\n    '> a': {\n      display: 'inline-flex',\n      textDecoration: 'none',\n      minHeight: px(64),\n      minWidth: px(64),\n\n      '&:hover': {\n        textDecoration: 'underline',\n      },\n    },\n\n    button: {\n      alignItems: 'center',\n      display: 'flex',\n      fontSize: px(16),\n      height: px(iconSize + 8),\n      justifyContent: 'center',\n      width: px(iconSize),\n    },\n  },\n  ({ style }: StyledProps) => {\n    const isCompactLayout = style.layout === 'compact';\n    const styles: CssLikeObject = {};\n\n    if (isCompactLayout) {\n      styles.borderBottom = `1px solid ${opacify(style.c, 0.6)}`;\n      styles['> a'] = {\n        display: 'flex',\n        margin: '0 auto',\n        maxWidth: px(640),\n        paddingBottom: '100%',\n        position: 'relative',\n\n        img: {\n          display: 'block',\n          bottom: 0,\n          left: 0,\n          maxWidth: '100%',\n          position: 'absolute',\n          right: 0,\n          top: 0,\n        },\n      };\n    } else {\n      styles.alignItems = 'center';\n      styles.display = 'flex';\n      styles.minHeight = px(80);\n      styles['@media (max-width: 767px)'] = {\n        borderBottom: `1px solid ${opacify(style.c, 0.6)}`,\n        paddingLeft: px(8),\n        display: 'none',\n        width: '100%',\n      };\n      styles.img = {\n        height: px(imageSize),\n        width: px(imageSize),\n      };\n      styles['&.rswp__active'] = {\n        '@media (max-width: 767px)': {\n          display: 'flex',\n        },\n      };\n    }\n\n    return {\n      button: {\n        color: style.c,\n\n        '&.rswp__active': {\n          color: style.activeColor,\n        },\n      },\n\n      ...styles,\n    };\n  },\n  'InfoRSWP',\n);\n\nconst ContentWrapper = styled('div')(\n  {\n    display: 'flex',\n    flexDirection: 'column',\n    justifyContent: 'center',\n\n    '> a': {\n      fontSize: px(22),\n      marginTop: px(4),\n    },\n  },\n  ({ style }: StyledProps) => {\n    const isCompactLayout = style.layout === 'compact';\n    const styles: CssLikeObject = {};\n\n    if (isCompactLayout) {\n      styles.padding = px(8);\n      styles.width = '100%';\n    } else {\n      styles.minHeight = px(imageSize);\n\n      if (!style.hideCoverArt) {\n        styles.marginLeft = px(8);\n        styles.width = `calc(100% - ${px(imageSize + 8)})`;\n      } else {\n        styles.width = '100%';\n      }\n    }\n\n    return styles;\n  },\n  'ContentWrapperRSWP',\n);\n\nconst Content = styled('div')(\n  {\n    display: 'flex',\n    justifyContent: 'start',\n\n    '[data-type=\"title-artist-wrapper\"]': {\n      overflow: 'hidden',\n\n      div: {\n        marginLeft: `-${px(8)}`,\n        whiteSpace: 'nowrap',\n      },\n    },\n\n    p: {\n      fontSize: px(14),\n      lineHeight: 1.3,\n      paddingLeft: px(8),\n      paddingRight: px(8),\n      width: '100%',\n\n      '&:nth-of-type(1)': {\n        alignItems: 'center',\n        display: 'inline-flex',\n      },\n\n      '&:nth-of-type(2)': {\n        fontSize: px(12),\n      },\n    },\n\n    span: {\n      display: 'inline-block',\n    },\n  },\n  ({ style }: StyledProps) => {\n    const maskImageColor = getBgColor(style.bgColor, style.trackNameColor);\n\n    return {\n      '[data-type=\"title-artist-wrapper\"]': {\n        color: style.trackNameColor,\n        maxWidth: `calc(100% - ${px(style.showSaveIcon ? iconSize : 0)})`,\n\n        div: {\n          '-webkit-mask-image': `linear-gradient(90deg,transparent 0, ${maskImageColor} 6px, ${maskImageColor} calc(100% - 12px),transparent)`,\n        },\n      },\n      p: {\n        '&:nth-of-type(1)': {\n          color: style.trackNameColor,\n\n          a: {\n            color: style.trackNameColor,\n          },\n        },\n\n        '&:nth-of-type(2)': {\n          color: style.trackArtistColor,\n\n          a: {\n            color: style.trackArtistColor,\n          },\n        },\n      },\n    };\n  },\n  'ContentRSWP',\n);\n\nfunction Info(props: Props) {\n  const {\n    hideAttribution,\n    hideCoverArt,\n    isActive,\n    layout,\n    locale,\n    onFavoriteStatusChange,\n    showSaveIcon,\n    styles: { activeColor, bgColor, color, height, trackArtistColor, trackNameColor },\n    token,\n    track: { artists = [], id, image, name, uri },\n    updateSavedStatus,\n  } = props;\n  const [isSaved, setIsSaved] = useState(false);\n  const isMounted = useRef(false);\n  const previousId = usePrevious(id);\n  const isCompactLayout = layout === 'compact';\n\n  const updateState = (state: boolean) => {\n    if (!isMounted.current) {\n      return;\n    }\n\n    setIsSaved(state);\n  };\n\n  const setStatus = async () => {\n    if (!isMounted.current) {\n      return;\n    }\n\n    if (updateSavedStatus && id) {\n      updateSavedStatus((newStatus: boolean) => {\n        updateState(newStatus);\n      });\n    }\n\n    const status = await checkTracksStatus(token, id);\n    const [isFavorite] = status || [false];\n\n    updateState(isFavorite);\n    onFavoriteStatusChange(isSaved);\n  };\n\n  useEffect(() => {\n    isMounted.current = true;\n\n    if (showSaveIcon && id) {\n      setStatus();\n    }\n\n    return () => {\n      isMounted.current = false;\n    };\n    // eslint-disable-next-line\n  }, []);\n\n  useEffect(() => {\n    if (showSaveIcon && previousId !== id && id) {\n      updateState(false);\n\n      setStatus();\n    }\n  });\n\n  const handleClickIcon = async () => {\n    if (isSaved) {\n      await removeTracks(token, id);\n      updateState(false);\n    } else {\n      await saveTracks(token, id);\n      updateState(true);\n    }\n\n    onFavoriteStatusChange(!isSaved);\n  };\n\n  const title = getSpotifyLinkTitle(name, locale.title);\n  let favorite;\n\n  if (showSaveIcon && id) {\n    favorite = (\n      <button\n        aria-label={isSaved ? locale.removeTrack : locale.saveTrack}\n        className={`ButtonRSWP${isSaved ? ' rswp__active' : ''}`}\n        onClick={handleClickIcon}\n        title={isSaved ? locale.removeTrack : locale.saveTrack}\n        type=\"button\"\n      >\n        {isSaved ? <Favorite /> : <FavoriteOutline />}\n      </button>\n    );\n  }\n\n  const content: Record<string, ReactNode> = {};\n  const classes = [];\n\n  if (isActive) {\n    classes.push('rswp__active');\n  }\n\n  if (isCompactLayout) {\n    content.image = <img alt={name} src={image} />;\n  }\n\n  if (!id) {\n    return <div />;\n  }\n\n  return (\n    <Wrapper\n      className={classes.join(' ')}\n      data-component-name=\"Info\"\n      style={{\n        activeColor,\n        c: color,\n        h: height,\n        layout,\n        showSaveIcon,\n      }}\n    >\n      {!hideCoverArt && (\n        <a\n          aria-label={title}\n          href={getSpotifyLink(uri)}\n          rel=\"noreferrer\"\n          target=\"_blank\"\n          title={title}\n        >\n          <img alt={name} src={image} />\n        </a>\n      )}\n      <ContentWrapper\n        style={{\n          hideCoverArt,\n          layout,\n          showSaveIcon,\n        }}\n      >\n        {!!name && (\n          <Content\n            style={{\n              bgColor,\n              layout,\n              showSaveIcon,\n              trackArtistColor,\n              trackNameColor,\n            }}\n          >\n            <div data-type=\"title-artist-wrapper\">\n              <div>\n                <p>\n                  <span>\n                    <a\n                      aria-label={title}\n                      href={getSpotifyLink(uri)}\n                      rel=\"noreferrer\"\n                      target=\"_blank\"\n                      title={title}\n                    >\n                      {name}\n                    </a>\n                  </span>\n                </p>\n                <p title={artists.map(d => d.name).join(', ')}>\n                  {artists.map((artist, index) => {\n                    const artistTitle = getSpotifyLinkTitle(artist.name, locale.title);\n\n                    return (\n                      <span key={artist.uri}>\n                        {index ? ', ' : ''}\n                        <a\n                          aria-label={artistTitle}\n                          href={getSpotifyLink(artist.uri)}\n                          rel=\"noreferrer\"\n                          target=\"_blank\"\n                          title={artistTitle}\n                        >\n                          {artist.name}\n                        </a>\n                      </span>\n                    );\n                  })}\n                </p>\n              </div>\n            </div>\n            {favorite}\n          </Content>\n        )}\n        {!hideAttribution && (\n          <a\n            aria-label=\"Play on Spotify\"\n            href={getSpotifyLink(uri)}\n            rel=\"noreferrer\"\n            target=\"_blank\"\n          >\n            <SpotifyLogo bgColor={bgColor} />\n          </a>\n        )}\n      </ContentWrapper>\n    </Wrapper>\n  );\n}\n\nexport default memo(Info);\n"
  },
  {
    "path": "src/components/Loader.tsx",
    "content": "import { keyframes, px, styled } from '~/modules/styled';\n\nimport { ComponentsProps, StyledProps } from '~/types';\n\nconst Wrapper = styled('div')(\n  {\n    alignItems: 'center',\n    display: 'flex',\n    jsutifyContent: 'center',\n    position: 'relative',\n\n    '> div': {\n      borderRadius: '50%',\n      borderStyle: 'solid',\n      borderWidth: 0,\n      boxSizing: 'border-box',\n      height: 0,\n      left: '50%',\n      position: 'absolute',\n      top: '50%',\n      transform: 'translate(-50%, -50%)',\n      width: 0,\n    },\n  },\n  ({ style }: StyledProps) => {\n    const pulse = keyframes!({\n      '0%': {\n        height: 0,\n        width: 0,\n      },\n\n      '30%': {\n        borderWidth: px(8),\n        height: px(style.loaderSize),\n        opacity: 1,\n        width: px(style.loaderSize),\n      },\n\n      '100%': {\n        borderWidth: 0,\n        height: px(style.loaderSize),\n        opacity: 0,\n        width: px(style.loaderSize),\n      },\n    });\n\n    return {\n      height: px(style.h),\n\n      '> div': {\n        animation: `${pulse} 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1)`,\n        borderColor: style.loaderColor,\n        height: px(style.loaderSize),\n        width: px(style.loaderSize),\n      },\n    };\n  },\n  'LoaderRSWP',\n);\n\nexport default function Loader({ styles: { height, loaderColor, loaderSize } }: ComponentsProps) {\n  return (\n    <Wrapper data-component-name=\"Loader\" style={{ h: height, loaderColor, loaderSize }}>\n      <div />\n    </Wrapper>\n  );\n}\n"
  },
  {
    "path": "src/components/Player.tsx",
    "content": "import { forwardRef } from 'react';\n\nimport { px } from '~/modules/styled';\n\nimport { ComponentsProps } from '~/types';\n\nconst Player = forwardRef<HTMLDivElement, ComponentsProps>((props, ref) => {\n  const {\n    children,\n    styles: { bgColor, height },\n    ...rest\n  } = props;\n\n  return (\n    <div\n      ref={ref}\n      className=\"PlayerRSWP\"\n      data-component-name=\"Player\"\n      style={{ background: bgColor, minHeight: px(height) }}\n      {...rest}\n    >\n      {children}\n    </div>\n  );\n});\n\nexport default Player;\n"
  },
  {
    "path": "src/components/Slider.tsx",
    "content": "import { memo } from 'react';\nimport RangeSlider, { RangeSliderPosition } from '@gilbarbara/react-range-slider';\n\nimport { millisecondsToTime } from '~/modules/helpers';\nimport { px, styled } from '~/modules/styled';\n\nimport { StyledProps, StylesOptions } from '~/types';\n\ninterface Props {\n  durationMs: number;\n  isMagnified: boolean;\n  onChangeRange: (position: number) => void;\n  onToggleMagnify: () => void;\n  position: number;\n  progressMs: number;\n  styles: StylesOptions;\n}\n\nconst Wrapper = styled('div')(\n  {\n    alignItems: 'center',\n    display: 'flex',\n    fontSize: px(12),\n    transition: 'height 0.3s',\n    zIndex: 10,\n  },\n  ({ style }: StyledProps) => ({\n    '[class^=\"rswp_\"]': {\n      color: style.c,\n      lineHeight: 1,\n      minWidth: px(32),\n    },\n\n    '.rswp_progress': {\n      marginRight: px(style.sliderHeight + 6),\n      textAlign: 'right',\n    },\n\n    '.rswp_duration': {\n      marginLeft: px(style.sliderHeight + 6),\n      textAlign: 'left',\n    },\n  }),\n  'SliderRSWP',\n);\n\nfunction Slider(props: Props) {\n  const { durationMs, isMagnified, onChangeRange, onToggleMagnify, position, progressMs, styles } =\n    props;\n\n  const handleChangeRange = async ({ x }: RangeSliderPosition) => {\n    onChangeRange(x);\n  };\n\n  const handleSize = styles.sliderHeight + 6;\n\n  return (\n    <Wrapper\n      data-component-name=\"Slider\"\n      data-position={position}\n      onMouseEnter={onToggleMagnify}\n      onMouseLeave={onToggleMagnify}\n      style={{\n        c: styles.color,\n        sliderHeight: styles.sliderHeight,\n      }}\n    >\n      <div className=\"rswp_progress\">{millisecondsToTime(progressMs)}</div>\n      <RangeSlider\n        axis=\"x\"\n        className=\"slider\"\n        data-component-name=\"progress-bar\"\n        onChange={handleChangeRange}\n        styles={{\n          options: {\n            thumbBorder: 0,\n            thumbBorderRadius: styles.sliderHandleBorderRadius,\n            thumbColor: styles.sliderHandleColor,\n            thumbSize: isMagnified ? handleSize + 4 : handleSize,\n            height: isMagnified ? styles.sliderHeight + 4 : styles.sliderHeight,\n            padding: 0,\n            rangeColor: styles.sliderColor,\n            trackBorderRadius: styles.sliderTrackBorderRadius,\n            trackColor: styles.sliderTrackColor,\n          },\n        }}\n        x={position}\n        xMax={100}\n        xMin={0}\n        xStep={0.1}\n      />\n      <div className=\"rswp_duration\">{millisecondsToTime(durationMs)}</div>\n    </Wrapper>\n  );\n}\n\nexport default memo(Slider);\n"
  },
  {
    "path": "src/components/SpotifyLogo.tsx",
    "content": "import { textColor } from 'colorizr';\n\ninterface Props {\n  bgColor: string;\n}\n\nexport default function SpotifyLogo({ bgColor, ...rest }: Props) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 512 160\" width=\"3.2em\" {...rest}>\n      <path\n        d=\"M79.655 0C35.664 0 0 35.663 0 79.654c0 43.993 35.664 79.653 79.655 79.653 43.996 0 79.656-35.66 79.656-79.653 0-43.988-35.66-79.65-79.657-79.65L79.655 0Zm36.53 114.884a4.963 4.963 0 0 1-6.83 1.646c-18.702-11.424-42.246-14.011-69.973-7.676a4.967 4.967 0 0 1-5.944-3.738 4.958 4.958 0 0 1 3.734-5.945c30.343-6.933 56.37-3.948 77.367 8.884a4.965 4.965 0 0 1 1.645 6.83Zm9.75-21.689c-1.799 2.922-5.622 3.845-8.543 2.047-21.41-13.16-54.049-16.972-79.374-9.284a6.219 6.219 0 0 1-7.75-4.138 6.22 6.22 0 0 1 4.141-7.745c28.929-8.778 64.892-4.526 89.48 10.583 2.92 1.798 3.843 5.622 2.045 8.538Zm.836-22.585C101.1 55.362 58.742 53.96 34.231 61.4c-3.936 1.194-8.098-1.028-9.29-4.964a7.453 7.453 0 0 1 4.965-9.294c28.137-8.542 74.912-6.892 104.469 10.655a7.441 7.441 0 0 1 2.606 10.209c-2.092 3.54-6.677 4.707-10.206 2.605h-.004Zm89.944 2.922c-13.754-3.28-16.198-5.581-16.198-10.418 0-4.57 4.299-7.645 10.7-7.645 6.202 0 12.347 2.336 18.796 7.143.19.145.437.203.675.165a.888.888 0 0 0 .6-.367l6.715-9.466a.903.903 0 0 0-.171-1.225c-7.676-6.157-16.313-9.15-26.415-9.15-14.848 0-25.225 8.911-25.225 21.662 0 13.673 8.95 18.515 24.417 22.252 13.155 3.031 15.38 5.57 15.38 10.11 0 5.032-4.49 8.161-11.718 8.161-8.028 0-14.582-2.71-21.906-9.046a.932.932 0 0 0-.656-.218.89.89 0 0 0-.619.313l-7.533 8.96a.906.906 0 0 0 .086 1.256c8.522 7.61 19.004 11.624 30.323 11.624 16 0 26.339-8.742 26.339-22.277.028-11.421-6.81-17.746-23.561-21.821l-.029-.013Zm59.792-13.564c-6.934 0-12.622 2.732-17.321 8.33v-6.3c0-.498-.4-.903-.894-.903h-12.318a.899.899 0 0 0-.894.902v70.009c0 .494.4.903.894.903h12.318a.901.901 0 0 0 .894-.903v-22.097c4.699 5.26 10.387 7.838 17.32 7.838 12.89 0 25.94-9.92 25.94-28.886.019-18.97-13.032-28.894-25.93-28.894l-.01.001Zm11.614 28.893c0 9.653-5.945 16.397-14.468 16.397-8.418 0-14.772-7.048-14.772-16.397 0-9.35 6.354-16.397 14.772-16.397 8.38 0 14.468 6.893 14.468 16.396Zm47.759-28.893c-16.598 0-29.601 12.78-29.601 29.1 0 16.143 12.917 28.784 29.401 28.784 16.655 0 29.696-12.736 29.696-28.991 0-16.2-12.955-28.89-29.496-28.89v-.003Zm0 45.385c-8.827 0-15.485-7.096-15.485-16.497 0-9.444 6.43-16.298 15.285-16.298 8.884 0 15.58 7.093 15.58 16.504 0 9.443-6.468 16.291-15.38 16.291Zm64.937-44.258h-13.554V47.24c0-.497-.4-.902-.894-.902H374.05a.906.906 0 0 0-.904.902v13.855h-5.916a.899.899 0 0 0-.894.902v10.584a.9.9 0 0 0 .894.903h5.916v27.39c0 11.062 5.508 16.674 16.38 16.674 4.413 0 8.075-.914 11.528-2.873a.88.88 0 0 0 .457-.78v-10.083a.896.896 0 0 0-.428-.76.873.873 0 0 0-.876-.039c-2.368 1.19-4.66 1.741-7.229 1.741-3.947 0-5.716-1.798-5.716-5.812V73.49h13.554a.899.899 0 0 0 .894-.903V62.003a.873.873 0 0 0-.884-.903l-.01-.005Zm47.217.054v-1.702c0-5.006 1.921-7.238 6.22-7.238 2.57 0 4.633.51 6.945 1.28a.895.895 0 0 0 1.18-.858l-.001-10.377a.891.891 0 0 0-.637-.865c-2.435-.726-5.555-1.47-10.235-1.47-11.367 0-17.388 6.405-17.388 18.516v2.606h-5.916a.906.906 0 0 0-.904.902v10.638c0 .497.41.903.904.903h5.916v42.237c0 .504.41.904.904.904h12.308c.504 0 .904-.4.904-.904V73.487h11.5l17.616 42.234c-1.998 4.433-3.967 5.317-6.65 5.317-2.168 0-4.46-.646-6.79-1.93a.98.98 0 0 0-.714-.067.896.896 0 0 0-.533.485l-4.175 9.16a.9.9 0 0 0 .39 1.17c4.356 2.359 8.284 3.367 13.145 3.367 9.093 0 14.125-4.242 18.548-15.637l21.364-55.204a.88.88 0 0 0-.095-.838.878.878 0 0 0-.733-.392h-12.822a.901.901 0 0 0-.856.605l-13.136 37.509-14.382-37.534a.898.898 0 0 0-.837-.58h-21.04v-.003Zm-27.375-.054h-12.318a.907.907 0 0 0-.903.902v53.724c0 .504.409.904.903.904h12.318c.495 0 .904-.4.904-.904v-53.72a.9.9 0 0 0-.904-.903v-.003Zm-6.088-24.464c-4.88 0-8.836 3.95-8.836 8.828a8.835 8.835 0 0 0 8.836 8.836c4.88 0 8.827-3.954 8.827-8.836a8.83 8.83 0 0 0-8.827-8.828Z\"\n        fill={textColor(bgColor)}\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/Volume.tsx",
    "content": "import { useCallback, useEffect, useRef, useState } from 'react';\nimport RangeSlider, { RangeSliderPosition } from '@gilbarbara/react-range-slider';\n\nimport { useMediaQuery, usePrevious } from '~/modules/hooks';\nimport { CssLikeObject, px, styled } from '~/modules/styled';\n\nimport { Layout, Locale, StyledProps, StylesOptions } from '~/types';\n\nimport ClickOutside from './ClickOutside';\nimport VolumeHigh from './icons/VolumeHigh';\nimport VolumeLow from './icons/VolumeLow';\nimport VolumeMid from './icons/VolumeMid';\nimport VolumeMute from './icons/VolumeMute';\n\ninterface Props {\n  inlineVolume: boolean;\n  layout: Layout;\n  locale: Locale;\n  playerPosition: string;\n  setVolume: (volume: number) => any;\n  styles: StylesOptions;\n  volume: number;\n}\n\nconst WrapperWithToggle = styled('div')(\n  {\n    display: 'none',\n    'pointer-events': 'all',\n    position: 'relative',\n    zIndex: 20,\n\n    '> div': {\n      alignItems: 'center',\n      backgroundColor: '#000',\n      borderRadius: px(4),\n      color: '#fff',\n      display: 'flex',\n      filter: 'drop-shadow(1px 1px 6px rgba(0, 0, 0, 0.5))',\n      flexDirection: 'column',\n      left: '-4px',\n      padding: px(16),\n      position: 'absolute',\n\n      '> span': {\n        background: 'transparent',\n        borderLeft: `6px solid transparent`,\n        borderRight: `6px solid transparent`,\n        content: '\"\"',\n        display: 'block',\n        height: 0,\n        position: 'absolute',\n        width: 0,\n      },\n    },\n\n    '> button': {\n      alignItems: 'center',\n      display: 'flex',\n      fontSize: px(24),\n      height: px(32),\n      justifyContent: 'center',\n      width: px(32),\n    },\n\n    '@media (any-pointer: fine)': {\n      display: 'block',\n    },\n  },\n  ({ style }: StyledProps) => {\n    const isCompact = style.layout === 'compact';\n    const spanStyles: CssLikeObject = isCompact\n      ? {\n          bottom: `-${px(6)}`,\n          borderTop: `6px solid #000`,\n        }\n      : {\n          [style.p === 'top' ? 'border-bottom' : 'border-top']: `6px solid #000`,\n          [style.p]: '-6px',\n        };\n\n    return {\n      '> button': {\n        color: style.c,\n      },\n      '> div': {\n        [isCompact ? 'bottom' : style.p]: '130%',\n\n        '> span': spanStyles,\n      },\n    };\n  },\n  'VolumeRSWP',\n);\n\nconst WrapperInline = styled('div')(\n  {\n    display: 'none',\n    padding: `0 ${px(8)}`,\n    'pointer-events': 'all',\n\n    '> div': {\n      display: 'flex',\n      padding: `0 ${px(5)}`,\n      width: px(100),\n    },\n\n    '> span': {\n      display: 'flex',\n      fontSize: px(24),\n    },\n\n    '@media (any-pointer: fine)': {\n      alignItems: 'center',\n      display: 'flex',\n    },\n  },\n  ({ style }) => ({\n    color: style.c,\n  }),\n  'VolumeInlineRSWP',\n);\n\nexport default function Volume(props: Props) {\n  const { inlineVolume, layout, locale, playerPosition, setVolume, styles, volume } = props;\n  const [isOpen, setIsOpen] = useState(false);\n  const [volumeState, setVolumeState] = useState(volume);\n  const timeoutRef = useRef<number>();\n  const previousVolume = usePrevious(volume);\n  const isMediumScreen = useMediaQuery('(min-width: 768px)');\n  const isInline = layout === 'responsive' && inlineVolume && isMediumScreen;\n\n  useEffect(() => {\n    if (previousVolume !== volume && volume !== volumeState) {\n      setVolumeState(volume);\n    }\n  }, [previousVolume, volume, volumeState]);\n\n  const handleClickToggleList = useCallback(() => {\n    setIsOpen(s => !s);\n  }, []);\n\n  const handleChangeSlider = ({ x, y }: RangeSliderPosition) => {\n    const value = isInline ? x : y;\n    const currentvolume = Math.round(value) / 100;\n\n    clearTimeout(timeoutRef.current);\n\n    timeoutRef.current = window.setTimeout(() => {\n      setVolume(currentvolume);\n    }, 250);\n\n    setVolumeState(currentvolume);\n  };\n\n  const handleAfterEnd = () => {\n    setTimeout(() => {\n      setIsOpen(false);\n    }, 100);\n  };\n\n  let icon = <VolumeHigh />;\n\n  if (volume === 0) {\n    icon = <VolumeMute />;\n  } else if (volume <= 0.4) {\n    icon = <VolumeLow />;\n  } else if (volume <= 0.7) {\n    icon = <VolumeMid />;\n  }\n\n  if (isInline) {\n    return (\n      <WrapperInline data-component-name=\"Volume\" data-value={volume} style={{ c: styles.color }}>\n        <span>{icon}</span>\n        <div>\n          <RangeSlider\n            axis=\"x\"\n            className=\"volume\"\n            data-component-name=\"volume-bar\"\n            onAfterEnd={handleAfterEnd}\n            onChange={handleChangeSlider}\n            styles={{\n              options: {\n                thumbBorder: 0,\n                thumbBorderRadius: styles.sliderHandleBorderRadius,\n                thumbColor: styles.sliderHandleColor,\n                height: 4,\n                padding: 0,\n                rangeColor: styles.sliderColor,\n                trackBorderRadius: styles.sliderTrackBorderRadius,\n                trackColor: styles.sliderTrackColor,\n              },\n            }}\n            x={volume * 100}\n            xMax={100}\n            xMin={0}\n          />\n        </div>\n      </WrapperInline>\n    );\n  }\n\n  return (\n    <ClickOutside isActive={isOpen} onClick={handleClickToggleList}>\n      <WrapperWithToggle\n        data-component-name=\"Volume\"\n        data-value={volume}\n        style={{ c: styles.color, layout, p: playerPosition }}\n      >\n        {isOpen && (\n          <div>\n            <RangeSlider\n              axis=\"y\"\n              className=\"volume\"\n              data-component-name=\"volume-bar\"\n              onAfterEnd={handleAfterEnd}\n              onChange={handleChangeSlider}\n              styles={{\n                options: {\n                  padding: 0,\n                  rangeColor: '#fff',\n                  thumbBorder: 0,\n                  thumbBorderRadius: 12,\n                  thumbColor: '#fff',\n                  thumbSize: 12,\n                  trackColor: 'rgba(255, 255, 255, 0.5)',\n                  width: 6,\n                },\n              }}\n              y={volume * 100}\n              yMax={100}\n              yMin={0}\n            />\n            <span />\n          </div>\n        )}\n        <button\n          aria-label={locale.volume}\n          className=\"ButtonRSWP\"\n          onClick={handleClickToggleList}\n          title={locale.volume}\n          type=\"button\"\n        >\n          {icon}\n        </button>\n      </WrapperWithToggle>\n    </ClickOutside>\n  );\n}\n"
  },
  {
    "path": "src/components/Wrapper.tsx",
    "content": "import { memo } from 'react';\n\nimport { CssLikeObject, px, styled } from '~/modules/styled';\n\nimport { ComponentsProps, StyledProps } from '~/types';\n\nconst StyledWrapper = styled('div')(\n  {\n    alignItems: 'center',\n    display: 'flex',\n    flexDirection: 'column',\n    flexWrap: 'wrap',\n    justifyContent: 'center',\n    position: 'relative',\n\n    '> *': {\n      width: '100%',\n    },\n  },\n  ({ style }: StyledProps) => {\n    let styles: CssLikeObject = {};\n\n    if (style.layout === 'responsive') {\n      styles = {\n        '> *': {\n          '@media (min-width: 768px)': {\n            width: '33.3333%',\n          },\n        },\n\n        '@media (min-width: 768px)': {\n          flexDirection: 'row',\n          padding: `0 ${px(8)}`,\n        },\n      };\n    }\n\n    return {\n      minHeight: px(style.h),\n      ...styles,\n    };\n  },\n  'WrapperRSWP',\n);\n\nfunction Wrapper(props: ComponentsProps) {\n  const { children, layout, styles } = props;\n\n  return (\n    <StyledWrapper data-component-name=\"Wrapper\" style={{ h: styles.height, layout }}>\n      {children}\n    </StyledWrapper>\n  );\n}\n\nexport default memo(Wrapper);\n"
  },
  {
    "path": "src/components/icons/Devices.tsx",
    "content": "export default function DevicesIcon(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M57 4c3.864 0 7 3.136 7 7v42a7 7 0 0 1-7 7H31a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h26ZM16 54v6H8v-6h8Zm41-44H31a1 1 0 0 0-1 1v42a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1ZM44 32a8 8 0 1 1 0 16 8 8 0 0 1 0-16ZM16 4v6H7a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h9v6H7a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h9Zm28 12a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/DevicesComputer.tsx",
    "content": "export default function DevicesComputerIcon(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M7.226 10.323a7.228 7.228 0 0 1 7.226-7.226h35.096a7.228 7.228 0 0 1 7.226 7.226V37.16a7.226 7.226 0 0 1-7.226 7.226H14.452a7.226 7.226 0 0 1-7.226-7.226V10.323Zm7.226-1.033c-.57 0-1.033.462-1.033 1.033V37.16c0 .57.463 1.033 1.033 1.033h35.096c.57 0 1.033-.463 1.033-1.033V10.323c0-.57-.463-1.033-1.033-1.033H14.452ZM0 57.806a3.097 3.097 0 0 1 3.097-3.096h57.806a3.097 3.097 0 0 1 0 6.193H3.097A3.097 3.097 0 0 1 0 57.806Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/DevicesMobile.tsx",
    "content": "export default function DevicesMobileIcon(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M44.8 0a9.6 9.6 0 0 1 9.6 9.6v44.8a9.6 9.6 0 0 1-9.6 9.6H19.2a9.6 9.6 0 0 1-9.6-9.6V9.6A9.6 9.6 0 0 1 19.2 0h25.6Zm0 6.4H19.2A3.2 3.2 0 0 0 16 9.6v44.8a3.2 3.2 0 0 0 3.2 3.2h25.6a3.2 3.2 0 0 0 3.2-3.2V9.6a3.2 3.2 0 0 0-3.2-3.2ZM32 43.2a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/DevicesSpeaker.tsx",
    "content": "export default function DevicesSpeakerIcon(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M45 4c3.864 0 7 3.136 7 7v42a7 7 0 0 1-7 7H19a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h26Zm0 6H19a1 1 0 0 0-1 1v42a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1ZM32 32a8 8 0 1 1 0 16 8 8 0 0 1 0-16Zm0-16a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/Favorite.tsx",
    "content": "export default function Favorite(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M63.673 16.52A17.676 17.676 0 0 0 49.197 2.563c-5.4-.861-10.891.852-14.844 4.63a3.43 3.43 0 0 1-4.672 0C22.956.689 12.305.62 5.498 7.039c-6.808 6.419-7.366 17.055-1.268 24.15l24.246 28.894a4.623 4.623 0 0 0 7.078 0L59.8 31.19a17.328 17.328 0 0 0 3.873-14.66v-.008Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/FavoriteOutline.tsx",
    "content": "export default function FavoriteOutline(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M5.944 7.206C13.271.3 24.723.34 31.999 7.3A18.924 18.924 0 0 1 48.02 2.32h.008a19.068 19.068 0 0 1 15.617 15.071v.013A18.759 18.759 0 0 1 59.47 33.26L37.573 59.353a7.288 7.288 0 0 1-8.642 1.916 7.276 7.276 0 0 1-2.498-1.912l-21.901-26.1c-6.55-7.671-5.93-19.131 1.408-26.051h.004Zm13.04 1.04a12.726 12.726 0 0 0-9.737 20.997l.021.02 21.905 26.105c.316.372.84.488 1.284.285.143-.066.27-.164.372-.285l21.934-26.137a12.565 12.565 0 0 0 2.808-10.625 12.875 12.875 0 0 0-10.534-10.17 12.714 12.714 0 0 0-10.785 3.37l-.029.029a6.198 6.198 0 0 1-8.444 0l-.037-.033a12.727 12.727 0 0 0-8.758-3.556Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/Next.tsx",
    "content": "export default function Next(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M53.486 0a3.2 3.2 0 0 0-3.2 3.2v23.543L4.8.489A3.2 3.2 0 0 0 0 3.255V60.74a3.2 3.2 0 0 0 4.8 2.774l45.486-26.262V60.8a3.2 3.2 0 0 0 3.2 3.2H60.8a3.2 3.2 0 0 0 3.2-3.2V3.2A3.2 3.2 0 0 0 60.8 0h-7.314Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/Pause.tsx",
    "content": "export default function Pause(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-5.4 18h-5.2a1.4 1.4 0 0 0-1.4 1.4v25.2a1.4 1.4 0 0 0 1.4 1.4h5.2a1.4 1.4 0 0 0 1.4-1.4V19.4a1.4 1.4 0 0 0-1.4-1.4Zm16 0h-5.2a1.4 1.4 0 0 0-1.4 1.4v25.2a1.4 1.4 0 0 0 1.4 1.4h5.2a1.4 1.4 0 0 0 1.4-1.4V19.4a1.4 1.4 0 0 0-1.4-1.4Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/Play.tsx",
    "content": "export default function Play(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-7.61 18.188c-.435.251-.702.715-.701 1.216v25.194a1.402 1.402 0 0 0 2.104 1.214L47.61 33.214a1.402 1.402 0 0 0 0-2.428L25.793 18.188c-.435-.25-.97-.25-1.404 0Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/Previous.tsx",
    "content": "export default function Previous(props: any) {\n  return (\n    <svg height=\"1em\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 64 64\" width=\"1em\" {...props}>\n      <path\n        d=\"M10.514 0a3.2 3.2 0 0 1 3.2 3.2v23.543L59.2.489A3.2 3.2 0 0 1 64 3.255V60.74a3.2 3.2 0 0 1-4.8 2.774L13.714 37.253V60.8a3.2 3.2 0 0 1-3.2 3.2H3.2A3.2 3.2 0 0 1 0 60.8V3.2A3.2 3.2 0 0 1 3.2 0h7.314Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/VolumeHigh.tsx",
    "content": "export default function VolumeHigh(props: any) {\n  return (\n    <svg\n      data-component-name=\"VolumeHigh\"\n      height=\"1em\"\n      preserveAspectRatio=\"xMidYMid\"\n      viewBox=\"0 0 64 64\"\n      width=\"1em\"\n      {...props}\n    >\n      <path\n        d=\"M37.963 3.402a2.989 2.989 0 0 1 1.5 2.596v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0ZM45 9.542a23.008 23.008 0 0 1 0 44.912V48.25a17.008 17.008 0 0 0 0-32.508Zm-11.532 1.656-23.2 13.4a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6ZM45 22.238a11 11 0 0 1 0 19.52v-19.52Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/VolumeLow.tsx",
    "content": "export default function VolumeLow(props: any) {\n  return (\n    <svg\n      data-component-name=\"VolumeLow\"\n      height=\"1em\"\n      preserveAspectRatio=\"xMidYMid\"\n      viewBox=\"0 0 64 64\"\n      width=\"1em\"\n      {...props}\n    >\n      <path\n        d=\"M37.963 3.398a3 3 0 0 1 1.5 2.6v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0v-.004Zm-27.696 21.2a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6l-23.2 13.4ZM45 41.758v-19.52a11 11 0 0 1 0 19.52Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/VolumeMid.tsx",
    "content": "export default function VolumeHigh(props: any) {\n  return (\n    <svg\n      data-component-name=\"VolumeMid\"\n      height=\"1em\"\n      preserveAspectRatio=\"xMidYMid\"\n      viewBox=\"0 0 64 64\"\n      width=\"1em\"\n      {...props}\n    >\n      <path\n        d=\"M37.963 3.398a3 3 0 0 1 1.5 2.6v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0v-.004Zm-27.696 21.2a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6l-23.2 13.4ZM45 48.946a18.008 18.008 0 0 0 0-33.896v6.6a11.996 11.996 0 0 1 0 20.7v6.596Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/icons/VolumeMute.tsx",
    "content": "export default function VolumeMute(props: any) {\n  return (\n    <svg\n      data-component-name=\"VolumeMute\"\n      height=\"1em\"\n      preserveAspectRatio=\"xMidYMid\"\n      viewBox=\"0 0 64 64\"\n      width=\"1em\"\n      {...props}\n    >\n      <path\n        d=\"M34.963 3.402a3 3 0 0 1 4.5 2.6v7.624a19.03 19.03 0 0 0-6 2.776v-5.2l-23.2 13.4a8.57 8.57 0 0 0-3.12 3.128 8.564 8.564 0 0 0 3.124 11.68l23.196 13.392v-5.2a18.92 18.92 0 0 0 6 2.776v7.624a3 3 0 0 1-4.5 2.596l-27.7-16a14.556 14.556 0 0 1-5.32-5.328C-2.06 32.313.32 23.428 7.263 19.402l27.7-16Zm17.354 17.6a3 3 0 0 1 2.122 5.12l-5.88 5.88 5.876 5.88a3 3 0 0 1-4.24 4.24l-5.88-5.88-5.88 5.88a3 3 0 1 1-4.385-4.095l6.025-6.025-5.876-5.88a3 3 0 0 1 4.236-4.24l5.88 5.88 5.88-5.88a3 3 0 0 1 2.122-.88Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/constants.ts",
    "content": "export const ERROR_TYPE = {\n  ACCOUNT: 'account',\n  AUTHENTICATION: 'authentication',\n  INITIALIZATION: 'initialization',\n  PLAYBACK: 'playback',\n  PLAYER: 'player',\n} as const;\n\nexport const SPOTIFY_CONTENT_TYPE = {\n  ALBUM: 'album',\n  ARTIST: 'artist',\n  PLAYLIST: 'playlist',\n  SHOW: 'show',\n  TRACK: 'track',\n};\n\nexport const STATUS = {\n  ERROR: 'ERROR',\n  IDLE: 'IDLE',\n  INITIALIZING: 'INITIALIZING',\n  READY: 'READY',\n  RUNNING: 'RUNNING',\n  UNSUPPORTED: 'UNSUPPORTED',\n} as const;\n\nexport const TRANSPARENT_COLOR = 'rgba(0, 0, 0, 0)';\n\nexport const TYPE = {\n  DEVICE: 'device_update',\n  FAVORITE: 'favorite_update',\n  PLAYER: 'player_update',\n  PRELOAD: 'preload_update',\n  PROGRESS: 'progress_update',\n  STATUS: 'status_update',\n  TRACK: 'track_update',\n} as const;\n"
  },
  {
    "path": "src/index.tsx",
    "content": "/* eslint-disable camelcase */\nimport { createRef, PureComponent, ReactNode } from 'react';\nimport isEqual from '@gilbarbara/deep-equal';\nimport memoize from 'memoize-one';\n\nimport { ERROR_TYPE, STATUS, TYPE } from '~/constants';\nimport {\n  getItemImage,\n  getLocale,\n  getMergedStyles,\n  getPreloadData,\n  getRepeatState,\n  getSpotifyURIType,\n  getTrackInfo,\n} from '~/modules/getters';\nimport { loadSpotifyPlayer, parseIds, parseVolume, round, validateURI } from '~/modules/helpers';\nimport {\n  getDevices,\n  getPlaybackState,\n  next,\n  pause,\n  play,\n  previous,\n  seek,\n  setDevice,\n  setVolume,\n} from '~/modules/spotify';\nimport { put } from '~/modules/styled';\n\nimport Actions from '~/components/Actions';\nimport Controls from '~/components/Controls';\nimport Devices from '~/components/Devices';\nimport ErrorMessage from '~/components/ErrorMessage';\nimport Info from '~/components/Info';\nimport Loader from '~/components/Loader';\nimport Player from '~/components/Player';\nimport Volume from '~/components/Volume';\nimport Wrapper from '~/components/Wrapper';\n\nimport {\n  CallbackState,\n  ErrorType,\n  Locale,\n  PlayOptions,\n  Props,\n  SpotifyArtist,\n  SpotifyDevice,\n  SpotifyPlayerCallback,\n  State,\n  Status,\n  StylesOptions,\n} from './types';\n\nput('.PlayerRSWP', {\n  boxSizing: 'border-box',\n  fontSize: 'inherit',\n  width: '100%',\n\n  '*': {\n    boxSizing: 'border-box',\n  },\n\n  p: {\n    margin: 0,\n  },\n});\n\nput('.ButtonRSWP', {\n  appearance: 'none',\n  background: 'transparent',\n  border: 0,\n  borderRadius: 0,\n  color: 'inherit',\n  cursor: 'pointer',\n  display: 'inline-flex',\n  lineHeight: 1,\n  padding: 0,\n\n  ':focus': {\n    outlineColor: '#000',\n    outlineOffset: 3,\n  },\n});\n\nexport type SpotifyPlayer = Spotify.Player;\n\nexport { ERROR_TYPE, STATUS, TYPE } from './constants';\nclass SpotifyWebPlayer extends PureComponent<Props, State> {\n  private isMounted = false;\n  private emptyTrack = {\n    artists: [] as SpotifyArtist[],\n    durationMs: 0,\n    id: '',\n    image: '',\n    name: '',\n    uri: '',\n  };\n\n  private locale: Locale;\n  private player?: Spotify.Player;\n  private playerProgressInterval?: number;\n  private playerSyncInterval?: number;\n  private ref = createRef<HTMLDivElement>();\n  private renderInlineActions = false;\n  private resizeTimeout?: number;\n  private seekUpdateInterval = 100;\n  private styles: StylesOptions;\n  private syncTimeout?: number;\n\n  private getPlayOptions = memoize((ids: string[]): PlayOptions => {\n    const playOptions: PlayOptions = {\n      context_uri: undefined,\n      uris: undefined,\n    };\n\n    if (ids) {\n      if (!ids.every(d => validateURI(d))) {\n        return playOptions;\n      }\n\n      if (ids.some(d => getSpotifyURIType(d) === 'track')) {\n        if (!ids.every(d => getSpotifyURIType(d) === 'track')) {\n          // eslint-disable-next-line no-console\n          console.warn(\"You can't mix tracks URIs with other types\");\n        }\n\n        playOptions.uris = ids.filter(d => validateURI(d) && getSpotifyURIType(d) === 'track');\n      } else {\n        if (ids.length > 1) {\n          // eslint-disable-next-line no-console\n          console.warn(\"Albums, Artists, Playlists and Podcasts can't have multiple URIs\");\n        }\n\n        // eslint-disable-next-line prefer-destructuring\n        playOptions.context_uri = ids[0];\n      }\n    }\n\n    return playOptions;\n  });\n\n  constructor(props: Props) {\n    super(props);\n\n    this.state = {\n      currentDeviceId: '',\n      currentURI: '',\n      deviceId: '',\n      devices: [],\n      error: '',\n      errorType: null,\n      isActive: false,\n      isInitializing: false,\n      isMagnified: false,\n      isPlaying: false,\n      isSaved: false,\n      isUnsupported: false,\n      needsUpdate: false,\n      nextTracks: [],\n      playerPosition: 'bottom',\n      position: 0,\n      previousTracks: [],\n      progressMs: 0,\n      repeat: 'off',\n      shuffle: false,\n      status: STATUS.IDLE,\n      track: this.emptyTrack,\n      volume: parseVolume(props.initialVolume) || 1,\n    };\n\n    this.locale = getLocale(props.locale);\n\n    this.styles = getMergedStyles(props.styles);\n  }\n\n  static defaultProps = {\n    autoPlay: false,\n    initialVolume: 1,\n    magnifySliderOnHover: false,\n    name: 'Spotify Web Player',\n    persistDeviceSelection: false,\n    showSaveIcon: false,\n    syncExternalDeviceInterval: 5,\n    syncExternalDevice: false,\n  };\n\n  public async componentDidMount() {\n    this.isMounted = true;\n    const { top = 0 } = this.ref.current?.getBoundingClientRect() ?? {};\n\n    this.updateState({\n      playerPosition: top > window.innerHeight / 2 ? 'bottom' : 'top',\n      status: STATUS.INITIALIZING,\n    });\n\n    if (!window.onSpotifyWebPlaybackSDKReady) {\n      window.onSpotifyWebPlaybackSDKReady = this.initializePlayer;\n    } else {\n      this.initializePlayer();\n    }\n\n    await loadSpotifyPlayer();\n\n    window.addEventListener('resize', this.handleResize);\n    this.handleResize();\n  }\n\n  public async componentDidUpdate(previousProps: Props, previousState: State) {\n    const { currentDeviceId, deviceId, isInitializing, isPlaying, repeat, shuffle, status, track } =\n      this.state;\n    const {\n      autoPlay,\n      layout,\n      locale,\n      offset,\n      play: playProp,\n      showSaveIcon,\n      styles,\n      syncExternalDevice,\n      uris,\n    } = this.props;\n    const isReady = previousState.status !== STATUS.READY && status === STATUS.READY;\n    const playOptions = this.getPlayOptions(parseIds(uris));\n\n    const canPlay = !!currentDeviceId && !!(playOptions.context_uri ?? playOptions.uris);\n    const shouldPlay = isReady && (autoPlay || playProp);\n\n    if (canPlay && shouldPlay) {\n      await this.togglePlay(true);\n\n      if (!isPlaying) {\n        this.updateState({ isPlaying: true });\n      }\n\n      if (this.isExternalPlayer) {\n        this.syncTimeout = window.setTimeout(() => {\n          this.syncDevice();\n        }, 600);\n      }\n    } else if (!isEqual(previousProps.uris, uris)) {\n      if (isPlaying || playProp) {\n        await this.togglePlay(true);\n      } else {\n        this.updateState({ needsUpdate: true });\n      }\n    } else if (previousProps.play !== playProp && playProp !== isPlaying) {\n      await this.togglePlay(!track.id);\n    }\n\n    if (previousState.status !== status) {\n      this.handleCallback({\n        ...this.state,\n        type: TYPE.STATUS,\n      });\n    }\n\n    if (previousState.currentDeviceId !== currentDeviceId && currentDeviceId) {\n      if (!isReady) {\n        this.handleCallback({\n          ...this.state,\n          type: TYPE.DEVICE,\n        });\n      }\n\n      await this.toggleSyncInterval(this.isExternalPlayer);\n      await this.updateSeekBar();\n    }\n\n    if (track.id && previousState.track.id !== track.id) {\n      this.handleCallback({\n        ...this.state,\n        type: TYPE.TRACK,\n      });\n\n      if (showSaveIcon) {\n        this.updateState({ isSaved: false });\n      }\n    }\n\n    if (previousState.isPlaying !== isPlaying) {\n      this.toggleProgressBar();\n      await this.toggleSyncInterval(this.isExternalPlayer);\n\n      this.handleCallback({\n        ...this.state,\n        type: TYPE.PLAYER,\n      });\n    }\n\n    if (previousState.repeat !== repeat || previousState.shuffle !== shuffle) {\n      this.handleCallback({\n        ...this.state,\n        type: TYPE.PLAYER,\n      });\n    }\n\n    if (previousProps.offset !== offset) {\n      await this.toggleOffset();\n    }\n\n    if (previousState.isInitializing && !isInitializing) {\n      if (syncExternalDevice && !uris) {\n        const playerState = await getPlaybackState(this.token);\n\n        if (playerState?.is_playing && playerState.device.id !== deviceId) {\n          this.setExternalDevice(playerState.device.id ?? '');\n        }\n      }\n    }\n\n    if (previousProps.layout !== layout) {\n      this.handleResize();\n    }\n\n    if (!isEqual(previousProps.locale, locale)) {\n      this.locale = getLocale(locale);\n    }\n\n    if (!isEqual(previousProps.styles, styles)) {\n      this.styles = getMergedStyles(styles);\n    }\n  }\n\n  public async componentWillUnmount() {\n    this.isMounted = false;\n\n    if (this.player) {\n      this.player.disconnect();\n    }\n\n    clearInterval(this.playerSyncInterval);\n    clearInterval(this.playerProgressInterval);\n    clearTimeout(this.syncTimeout);\n\n    window.removeEventListener('resize', this.handleResize);\n  }\n\n  private handleCallback(state: CallbackState): void {\n    const { callback } = this.props;\n\n    if (callback) {\n      callback(state);\n    }\n  }\n\n  private handleChangeRange = async (position: number) => {\n    const { track } = this.state;\n    const { callback } = this.props;\n    let progress = 0;\n\n    try {\n      const percentage = position / 100;\n\n      let stateChanges = {};\n\n      if (this.isExternalPlayer) {\n        progress = Math.round(track.durationMs * percentage);\n\n        await seek(this.token, progress);\n\n        stateChanges = {\n          position,\n          progressMs: progress,\n        };\n      } else if (this.player) {\n        const state = await this.player.getCurrentState();\n\n        if (state) {\n          progress = Math.round(state.track_window.current_track.duration_ms * percentage);\n          await this.player.seek(progress);\n\n          stateChanges = {\n            position,\n            progressMs: progress,\n          };\n        } else {\n          stateChanges = { position: 0 };\n        }\n      }\n\n      this.updateState(stateChanges);\n\n      if (callback) {\n        callback({\n          ...this.state,\n          ...stateChanges,\n          type: TYPE.PROGRESS,\n        });\n      }\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  };\n\n  private handleClickTogglePlay = async () => {\n    const { isActive } = this.state;\n\n    try {\n      await this.togglePlay(!this.isExternalPlayer && !isActive);\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  };\n\n  private handleClickPrevious = async () => {\n    try {\n      if (this.isExternalPlayer) {\n        await previous(this.token);\n        this.syncTimeout = window.setTimeout(() => {\n          this.syncDevice();\n        }, 300);\n      } else if (this.player) {\n        await this.player.previousTrack();\n      }\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  };\n\n  private handleClickNext = async () => {\n    try {\n      if (this.isExternalPlayer) {\n        await next(this.token);\n        this.syncTimeout = window.setTimeout(() => {\n          this.syncDevice();\n        }, 300);\n      } else if (this.player) {\n        await this.player.nextTrack();\n      }\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  };\n\n  private handleClickDevice = async (deviceId: string) => {\n    const { isUnsupported } = this.state;\n    const { autoPlay, persistDeviceSelection } = this.props;\n\n    this.updateState({ currentDeviceId: deviceId });\n\n    try {\n      await setDevice(this.token, deviceId);\n\n      if (persistDeviceSelection) {\n        sessionStorage.setItem('rswpDeviceId', deviceId);\n      }\n\n      if (isUnsupported) {\n        await this.syncDevice();\n\n        const playerState = await getPlaybackState(this.token);\n\n        if (playerState && !playerState.is_playing && autoPlay) {\n          await this.togglePlay(true);\n        }\n      }\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  };\n\n  private handleFavoriteStatusChange = (status: boolean) => {\n    const { isSaved } = this.state;\n\n    this.updateState({ isSaved: status });\n\n    if (isSaved !== status) {\n      this.handleCallback({\n        ...this.state,\n        isSaved: status,\n        type: TYPE.FAVORITE,\n      });\n    }\n  };\n\n  private handlePlayerErrors = async (type: ErrorType, message: string) => {\n    const { status } = this.state;\n    const isPlaybackError = type === ERROR_TYPE.PLAYBACK;\n    const isInitializationError = type === ERROR_TYPE.INITIALIZATION;\n\n    let nextStatus = status;\n    let devices: SpotifyDevice[] = [];\n\n    if (this.player && !isPlaybackError) {\n      this.player.disconnect();\n      this.player = undefined;\n    }\n\n    if (isInitializationError) {\n      nextStatus = STATUS.UNSUPPORTED;\n\n      ({ devices = [] } = await getDevices(this.token));\n    } else if (!isPlaybackError) {\n      nextStatus = STATUS.ERROR;\n    }\n\n    this.updateState({\n      devices,\n      error: message,\n      errorType: type,\n      isInitializing: false,\n      isUnsupported: isInitializationError,\n      status: nextStatus,\n    });\n  };\n\n  private handlePlayerStateChanges = async (state: Spotify.PlaybackState) => {\n    const { currentURI } = this.state;\n\n    try {\n      if (state) {\n        const {\n          paused,\n          position,\n          repeat_mode,\n          shuffle,\n          track_window: { current_track, next_tracks, previous_tracks },\n        } = state;\n\n        const isPlaying = !paused;\n        const volume = (await this.player?.getVolume()) ?? 100;\n        let trackState = {};\n\n        if ((!currentURI || currentURI !== current_track.uri) && current_track) {\n          trackState = {\n            currentURI: current_track.uri,\n            nextTracks: next_tracks.map(getTrackInfo),\n            position: 0,\n            previousTracks: previous_tracks.map(getTrackInfo),\n            track: getTrackInfo(current_track),\n          };\n        }\n\n        this.updateState({\n          error: '',\n          errorType: null,\n          isActive: true,\n          isPlaying,\n          progressMs: position,\n          repeat: getRepeatState(repeat_mode),\n          shuffle,\n          volume: round(volume),\n          ...trackState,\n        });\n      } else if (this.isExternalPlayer) {\n        await this.syncDevice();\n      } else {\n        this.updateState({\n          isActive: false,\n          isPlaying: false,\n          nextTracks: [],\n          position: 0,\n          previousTracks: [],\n          track: {\n            artists: [],\n            durationMs: 0,\n            id: '',\n            image: '',\n            name: '',\n            uri: '',\n          },\n        });\n      }\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  };\n\n  private handlePlayerStatus = async ({ device_id }: Spotify.WebPlaybackInstance) => {\n    const { currentDeviceId, devices } = await this.initializeDevices(device_id);\n\n    this.updateState({\n      currentDeviceId,\n      deviceId: device_id,\n      devices,\n      isInitializing: false,\n      status: device_id ? STATUS.READY : STATUS.IDLE,\n    });\n\n    if (device_id) {\n      await this.preload();\n    }\n  };\n\n  private handleResize = () => {\n    const { layout = 'responsive' } = this.props;\n\n    clearTimeout(this.resizeTimeout);\n\n    this.resizeTimeout = window.setTimeout(() => {\n      this.renderInlineActions = window.innerWidth >= 768 && layout === 'responsive';\n      this.forceUpdate();\n    }, 100);\n  };\n\n  private handleToggleMagnify = () => {\n    const { magnifySliderOnHover } = this.props;\n\n    if (magnifySliderOnHover) {\n      this.updateState(previousState => {\n        return { isMagnified: !previousState.isMagnified };\n      });\n    }\n  };\n\n  private get token(): string {\n    const { token } = this.props;\n\n    return token;\n  }\n\n  private async initializeDevices(id: string) {\n    const { persistDeviceSelection } = this.props;\n    const { devices } = await getDevices(this.token);\n    let currentDeviceId = id;\n\n    if (persistDeviceSelection) {\n      const savedDeviceId = sessionStorage.getItem('rswpDeviceId');\n\n      if (!savedDeviceId || !devices.some((d: SpotifyDevice) => d.id === savedDeviceId)) {\n        sessionStorage.setItem('rswpDeviceId', currentDeviceId);\n      } else {\n        currentDeviceId = savedDeviceId;\n      }\n    }\n\n    return { currentDeviceId, devices };\n  }\n\n  private initializePlayer = () => {\n    const { volume } = this.state;\n    const {\n      getOAuthToken = (callback: SpotifyPlayerCallback) => {\n        callback(this.token);\n      },\n      getPlayer,\n      name = 'Spotify Web Player',\n    } = this.props;\n\n    if (!window.Spotify) {\n      return;\n    }\n\n    this.updateState({\n      error: '',\n      errorType: null,\n      isInitializing: true,\n    });\n\n    this.player = new window.Spotify.Player({\n      getOAuthToken,\n      name,\n      volume,\n    });\n\n    this.player.addListener('ready', this.handlePlayerStatus);\n    this.player.addListener('not_ready', this.handlePlayerStatus);\n    this.player.addListener('player_state_changed', this.handlePlayerStateChanges);\n    this.player.addListener('initialization_error', error =>\n      this.handlePlayerErrors(ERROR_TYPE.INITIALIZATION, error.message),\n    );\n    this.player.addListener('authentication_error', error =>\n      this.handlePlayerErrors(ERROR_TYPE.AUTHENTICATION, error.message),\n    );\n    this.player.addListener('account_error', error =>\n      this.handlePlayerErrors(ERROR_TYPE.ACCOUNT, error.message),\n    );\n    this.player.addListener('playback_error', error =>\n      this.handlePlayerErrors(ERROR_TYPE.PLAYBACK, error.message),\n    );\n    this.player.addListener('autoplay_failed', async () => {\n      // eslint-disable-next-line no-console\n      console.log('Autoplay is not allowed by the browser autoplay rules');\n    });\n\n    this.player.connect();\n\n    if (getPlayer) {\n      getPlayer(this.player);\n    }\n  };\n\n  private get isExternalPlayer(): boolean {\n    const { currentDeviceId, deviceId, status } = this.state;\n\n    return (currentDeviceId && currentDeviceId !== deviceId) || status === STATUS.UNSUPPORTED;\n  }\n\n  private preload = async () => {\n    const { offset = 0, preloadData, uris } = this.props;\n\n    if (!preloadData) {\n      return;\n    }\n\n    const track = await getPreloadData(this.token, uris, offset);\n\n    if (track) {\n      this.updateState({ track }, () => {\n        this.handleCallback({\n          ...this.state,\n          type: TYPE.PRELOAD,\n        });\n      });\n    }\n  };\n\n  private setExternalDevice = (id: string) => {\n    this.updateState({ currentDeviceId: id, isPlaying: true });\n  };\n\n  private setVolume = async (volume: number) => {\n    if (this.isExternalPlayer) {\n      await setVolume(this.token, Math.round(volume * 100));\n      await this.syncDevice();\n    } else if (this.player) {\n      await this.player.setVolume(volume);\n    }\n\n    this.updateState({ volume });\n  };\n\n  private syncDevice = async () => {\n    if (!this.isMounted) {\n      return;\n    }\n\n    const { deviceId } = this.state;\n\n    try {\n      const playerState = await getPlaybackState(this.token);\n      let track = this.emptyTrack;\n\n      if (!playerState) {\n        throw new Error('No player');\n      }\n\n      if (playerState.item) {\n        track = {\n          artists: 'artists' in playerState.item ? playerState.item.artists : [],\n          durationMs: playerState.item.duration_ms,\n          id: playerState.item.id,\n          image: 'album' in playerState.item ? getItemImage(playerState.item.album) : '',\n          name: playerState.item.name,\n          uri: playerState.item.uri,\n        };\n      }\n\n      this.updateState({\n        error: '',\n        errorType: null,\n        isActive: true,\n        isPlaying: playerState.is_playing,\n        nextTracks: [],\n        previousTracks: [],\n        progressMs: playerState.item ? playerState.progress_ms ?? 0 : 0,\n        status: STATUS.READY,\n        track,\n        volume: parseVolume(playerState.device.volume_percent),\n      });\n    } catch (error: any) {\n      const state = {\n        isActive: false,\n        isPlaying: false,\n        position: 0,\n        track: this.emptyTrack,\n      };\n\n      if (deviceId) {\n        this.updateState({\n          currentDeviceId: deviceId,\n          ...state,\n        });\n\n        return;\n      }\n\n      this.updateState({\n        error: error.message,\n        errorType: ERROR_TYPE.PLAYER,\n        status: STATUS.ERROR,\n        ...state,\n      });\n    }\n  };\n\n  private async toggleSyncInterval(shouldSync: boolean) {\n    const { syncExternalDeviceInterval } = this.props;\n\n    try {\n      if (this.isExternalPlayer && shouldSync && !this.playerSyncInterval) {\n        await this.syncDevice();\n\n        clearInterval(this.playerSyncInterval);\n        this.playerSyncInterval = window.setInterval(\n          this.syncDevice,\n          syncExternalDeviceInterval! * 1000,\n        );\n      }\n\n      if ((!shouldSync || !this.isExternalPlayer) && this.playerSyncInterval) {\n        clearInterval(this.playerSyncInterval);\n        this.playerSyncInterval = undefined;\n      }\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  }\n\n  private toggleProgressBar() {\n    const { isPlaying } = this.state;\n\n    if (isPlaying) {\n      if (!this.playerProgressInterval) {\n        this.playerProgressInterval = window.setInterval(\n          this.updateSeekBar,\n          this.seekUpdateInterval,\n        );\n      }\n    } else if (this.playerProgressInterval) {\n      clearInterval(this.playerProgressInterval);\n      this.playerProgressInterval = undefined;\n    }\n  }\n\n  private toggleOffset = async () => {\n    const { currentDeviceId } = this.state;\n    const { offset, uris } = this.props;\n    const playOptions = this.getPlayOptions(parseIds(uris));\n\n    if (typeof offset === 'number') {\n      await play(this.token, { deviceId: currentDeviceId, offset, ...playOptions });\n    }\n  };\n\n  private togglePlay = async (force = false) => {\n    const { currentDeviceId, isPlaying, needsUpdate } = this.state;\n    const { offset, uris } = this.props;\n    const shouldInitialize = force || needsUpdate;\n    const playOptions = this.getPlayOptions(parseIds(uris));\n\n    try {\n      if (this.isExternalPlayer) {\n        if (!isPlaying) {\n          await play(this.token, {\n            deviceId: currentDeviceId,\n            offset,\n            ...(shouldInitialize ? playOptions : undefined),\n          });\n        } else {\n          await pause(this.token);\n\n          this.updateState({ isPlaying: false });\n        }\n\n        this.syncTimeout = window.setTimeout(() => {\n          this.syncDevice();\n        }, 300);\n      } else if (this.player) {\n        await this.player.activateElement();\n\n        const playerState = await this.player.getCurrentState();\n        const shouldPlay = !playerState && !!(playOptions.context_uri ?? playOptions.uris);\n\n        if (shouldPlay || shouldInitialize) {\n          await play(this.token, {\n            deviceId: currentDeviceId,\n            offset,\n            ...(shouldInitialize ? playOptions : undefined),\n          });\n          await this.player.togglePlay();\n        } else {\n          await this.player.togglePlay();\n        }\n      }\n\n      if (needsUpdate) {\n        this.updateState({ needsUpdate: false });\n      }\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  };\n\n  private updateSeekBar = async () => {\n    if (!this.isMounted) {\n      return;\n    }\n\n    const { progressMs, track } = this.state;\n\n    try {\n      if (this.isExternalPlayer) {\n        let position = progressMs / track.durationMs;\n\n        position = Number(((Number.isFinite(position) ? position : 0) * 100).toFixed(1));\n\n        this.updateState({\n          position,\n          progressMs: progressMs + this.seekUpdateInterval,\n        });\n      } else if (this.player) {\n        const state = await this.player.getCurrentState();\n\n        if (state) {\n          const progress = state.position;\n          const position = Number(\n            ((progress / state.track_window.current_track.duration_ms) * 100).toFixed(1),\n          );\n\n          this.updateState({\n            position,\n            progressMs: progress + this.seekUpdateInterval,\n          });\n        }\n      }\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error);\n    }\n  };\n\n  private updateState: typeof this.setState = (state, callback) => {\n    if (!this.isMounted) {\n      return;\n    }\n\n    this.setState(state, callback);\n  };\n\n  public render() {\n    const {\n      currentDeviceId,\n      deviceId,\n      devices,\n      error,\n      isActive,\n      isMagnified,\n      isPlaying,\n      isUnsupported,\n      nextTracks,\n      playerPosition,\n      position,\n      progressMs,\n      status,\n      track,\n      volume,\n    } = this.state;\n    const {\n      components,\n      hideAttribution = false,\n      hideCoverArt = false,\n      inlineVolume = true,\n      layout = 'responsive',\n      showSaveIcon,\n      updateSavedStatus,\n    } = this.props;\n    const isReady = ([STATUS.READY, STATUS.UNSUPPORTED] as Status[]).includes(status);\n\n    const output: Record<string, ReactNode> = {\n      main: <Loader styles={this.styles} />,\n    };\n\n    if (isReady) {\n      if (!output.info) {\n        output.info = (\n          <Info\n            hideAttribution={hideAttribution}\n            hideCoverArt={hideCoverArt}\n            isActive={isActive}\n            layout={layout}\n            locale={this.locale}\n            onFavoriteStatusChange={this.handleFavoriteStatusChange}\n            showSaveIcon={showSaveIcon!}\n            styles={this.styles}\n            token={this.token}\n            track={track}\n            updateSavedStatus={updateSavedStatus}\n          />\n        );\n      }\n\n      output.devices = (\n        <Devices\n          currentDeviceId={currentDeviceId}\n          deviceId={deviceId}\n          devices={devices}\n          layout={layout}\n          locale={this.locale}\n          onClickDevice={this.handleClickDevice}\n          open={isUnsupported && !deviceId}\n          playerPosition={playerPosition}\n          styles={this.styles}\n        />\n      );\n\n      output.volume = currentDeviceId ? (\n        <Volume\n          inlineVolume={inlineVolume}\n          layout={layout}\n          locale={this.locale}\n          playerPosition={playerPosition}\n          setVolume={this.setVolume}\n          styles={this.styles}\n          volume={volume}\n        />\n      ) : null;\n\n      if (this.renderInlineActions) {\n        output.actions = (\n          <Actions layout={layout} styles={this.styles}>\n            {output.devices}\n            {output.volume}\n          </Actions>\n        );\n      }\n\n      output.controls = (\n        <Controls\n          components={components}\n          devices={this.renderInlineActions ? null : output.devices}\n          durationMs={track.durationMs}\n          isActive={isActive}\n          isExternalDevice={this.isExternalPlayer}\n          isMagnified={isMagnified}\n          isPlaying={isPlaying}\n          layout={layout}\n          locale={this.locale}\n          nextTracks={nextTracks}\n          onChangeRange={this.handleChangeRange}\n          onClickNext={this.handleClickNext}\n          onClickPrevious={this.handleClickPrevious}\n          onClickTogglePlay={this.handleClickTogglePlay}\n          onToggleMagnify={this.handleToggleMagnify}\n          position={position}\n          progressMs={progressMs}\n          styles={this.styles}\n          volume={this.renderInlineActions ? null : output.volume}\n        />\n      );\n\n      output.main = (\n        <Wrapper layout={layout} styles={this.styles}>\n          {output.info}\n          {output.controls}\n          {output.actions}\n        </Wrapper>\n      );\n    } else if (output.info) {\n      output.main = output.info;\n    }\n\n    if (status === STATUS.ERROR) {\n      output.main = <ErrorMessage styles={this.styles}>{error}</ErrorMessage>;\n    }\n\n    return (\n      <Player ref={this.ref} data-ready={isReady} styles={this.styles}>\n        {output.main}\n      </Player>\n    );\n  }\n}\n\nexport * as spotifyApi from './modules/spotify';\nexport * from './types';\n\nexport default SpotifyWebPlayer;\n"
  },
  {
    "path": "src/modules/getters.ts",
    "content": "/* eslint-disable camelcase */\nimport { SPOTIFY_CONTENT_TYPE, TRANSPARENT_COLOR } from '~/constants';\nimport { parseIds, validateURI } from '~/modules/helpers';\nimport {\n  getAlbumTracks,\n  getArtistTopTracks,\n  getPlaylistTracks,\n  getShow,\n  getShowEpisodes,\n  getTrack,\n} from '~/modules/spotify';\n\nimport { IDs, Locale, RepeatState, SpotifyTrack, StylesOptions, StylesProps } from '~/types';\n\nexport function getBgColor(bgColor: string, fallbackColor?: string): string {\n  if (fallbackColor) {\n    return bgColor === TRANSPARENT_COLOR ? fallbackColor : bgColor;\n  }\n\n  return bgColor === 'transparent' ? TRANSPARENT_COLOR : bgColor;\n}\n\nexport function getItemImage(item: { images: Spotify.Image[] }): string {\n  const maxWidth = Math.max(...item.images.map(d => d.width ?? 0));\n\n  return item.images.find(d => d.width === maxWidth)?.url ?? '';\n}\n\nexport function getLocale(locale?: Partial<Locale>): Locale {\n  return {\n    currentDevice: 'Current device',\n    devices: 'Devices',\n    next: 'Next',\n    otherDevices: 'Select other device',\n    pause: 'Pause',\n    play: 'Play',\n    previous: 'Previous',\n    removeTrack: 'Remove from your favorites',\n    saveTrack: 'Save to your favorites',\n    title: '{name} on SPOTIFY',\n    volume: 'Volume',\n    ...locale,\n  };\n}\n\nexport function getMergedStyles(styles?: StylesProps): StylesOptions {\n  const mergedStyles = {\n    activeColor: '#1cb954',\n    bgColor: '#fff',\n    color: '#333',\n    errorColor: '#ff0026',\n    height: 80,\n    loaderColor: '#ccc',\n    loaderSize: 32,\n    sliderColor: '#666',\n    sliderHandleBorderRadius: '50%',\n    sliderHandleColor: '#000',\n    sliderHeight: 4,\n    sliderTrackBorderRadius: 4,\n    sliderTrackColor: '#ccc',\n    trackArtistColor: '#666',\n    trackNameColor: '#333',\n    ...styles,\n  };\n\n  mergedStyles.bgColor = getBgColor(mergedStyles.bgColor);\n\n  return mergedStyles;\n}\n\nexport async function getPreloadData(\n  token: string,\n  uris: IDs,\n  offset: number,\n): Promise<SpotifyTrack | null> {\n  const parsedURIs = parseIds(uris);\n  const uri = parsedURIs[offset];\n\n  if (!validateURI(uri)) {\n    if (process.env.NODE_ENV !== 'production') {\n      // eslint-disable-next-line no-console\n      console.error('PreloadData: Invalid URI', parsedURIs[offset]);\n    }\n\n    return null;\n  }\n\n  const [, type, id] = uri.split(':');\n\n  try {\n    switch (type) {\n      case SPOTIFY_CONTENT_TYPE.ALBUM: {\n        const { items } = await getAlbumTracks(token, id);\n        const track = await getTrack(token, items[offset].id);\n\n        return getTrackInfo(track);\n      }\n      case SPOTIFY_CONTENT_TYPE.ARTIST: {\n        const { tracks } = await getArtistTopTracks(token, id);\n\n        return getTrackInfo(tracks[offset]);\n      }\n      case SPOTIFY_CONTENT_TYPE.PLAYLIST: {\n        const { items } = await getPlaylistTracks(token, id);\n\n        if (items[offset]?.track) {\n          return getTrackInfo(items[offset]?.track);\n        }\n\n        return null;\n      }\n      case SPOTIFY_CONTENT_TYPE.SHOW: {\n        const show = await getShow(token, id);\n        const { items } = await getShowEpisodes(\n          token,\n          id,\n          show.total_episodes ? show.total_episodes - 1 : 0,\n        );\n\n        const episode = items?.[0] ?? {\n          duration_ms: 0,\n          id: show.id,\n          images: show.images,\n          name: show.name,\n          uri: show.uri,\n        };\n\n        return {\n          artists: [{ name: show.name, uri: show.uri }],\n          durationMs: episode.duration_ms,\n          id: episode.id,\n          image: getItemImage(episode),\n          name: episode.name,\n          uri: episode.uri,\n        };\n      }\n      default: {\n        const track = await getTrack(token, id);\n\n        return getTrackInfo(track);\n      }\n    }\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error('PreloadData:', error);\n\n    return null;\n  }\n}\n\nexport function getRepeatState(mode: number): RepeatState {\n  switch (mode) {\n    case 1:\n      return 'context';\n    case 2:\n      return 'track';\n    case 0:\n    default:\n      return 'off';\n  }\n}\n\nexport function getSpotifyLink(uri: string): string {\n  const [, type = '', id = ''] = uri.split(':');\n\n  return `https://open.spotify.com/${type}/${id}`;\n}\n\nexport function getSpotifyLinkTitle(name: string, locale: string): string {\n  return locale.replace('{name}', name);\n}\n\nexport function getSpotifyURIType(uri: string): string {\n  const [, type = ''] = uri.split(':');\n\n  return type;\n}\n\nexport function getTrackInfo(track: Spotify.Track | SpotifyApi.TrackObjectFull): SpotifyTrack {\n  const { album, artists, duration_ms, id, name, uri } = track;\n\n  return {\n    artists,\n    durationMs: duration_ms,\n    id: id ?? '',\n    image: getItemImage(album),\n    name,\n    uri,\n  };\n}\n"
  },
  {
    "path": "src/modules/helpers.ts",
    "content": "import { SPOTIFY_CONTENT_TYPE } from '~/constants';\n\nimport { IDs } from '~/types';\n\nexport function isNumber(value: unknown): value is number {\n  return typeof value === 'number';\n}\n\nexport function loadSpotifyPlayer(): Promise<any> {\n  return new Promise<void>((resolve, reject) => {\n    const scriptTag = document.getElementById('spotify-player');\n\n    if (!scriptTag) {\n      const script = document.createElement('script');\n\n      script.id = 'spotify-player';\n      script.type = 'text/javascript';\n      script.async = false;\n      script.defer = true;\n      script.src = 'https://sdk.scdn.co/spotify-player.js';\n      script.onload = () => resolve();\n      script.onerror = (error: any) => reject(new Error(`loadScript: ${error.message}`));\n\n      document.head.appendChild(script);\n    } else {\n      resolve();\n    }\n  });\n}\n\nexport function millisecondsToTime(input: number) {\n  const seconds = Math.floor((input / 1000) % 60);\n  const minutes = Math.floor((input / (1000 * 60)) % 60);\n  const hours = Math.floor((input / (1000 * 60 * 60)) % 24);\n\n  const parts: string[] = [];\n\n  if (hours > 0) {\n    parts.push(\n      `${hours}`.padStart(2, '0'),\n      `${minutes}`.padStart(2, '0'),\n      `${seconds}`.padStart(2, '0'),\n    );\n  } else {\n    parts.push(`${minutes}`, `${seconds}`.padStart(2, '0'));\n  }\n\n  return parts.join(':');\n}\n\nexport function parseIds(ids: IDs): string[] {\n  if (!ids) {\n    return [];\n  }\n\n  return Array.isArray(ids) ? ids : [ids];\n}\n\nexport function parseVolume(value?: unknown): number {\n  if (!isNumber(value)) {\n    return 1;\n  }\n\n  if (value > 1) {\n    return value / 100;\n  }\n\n  return value;\n}\n\n/**\n * Round decimal numbers\n */\nexport function round(number: number, digits = 2) {\n  const factor = 10 ** digits;\n\n  return Math.round(number * factor) / factor;\n}\n\nexport function validateURI(input: string): boolean {\n  if (input && input.indexOf(':') > -1) {\n    const [key, type, id] = input.split(':');\n\n    if (\n      key === 'spotify' &&\n      Object.values(SPOTIFY_CONTENT_TYPE).includes(type) &&\n      id.length === 22\n    ) {\n      return true;\n    }\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "src/modules/hooks.ts",
    "content": "import { useEffect, useRef, useState } from 'react';\n\nexport function useMediaQuery(input: string): boolean {\n  const getMatches = (query: string): boolean => {\n    return window.matchMedia(query).matches;\n  };\n\n  const [matches, setMatches] = useState<boolean>(getMatches(input));\n\n  function handleChange() {\n    setMatches(getMatches(input));\n  }\n\n  useEffect(() => {\n    const matchMedia = window.matchMedia(input);\n\n    // Triggered at the first client-side load and if query changes\n    handleChange();\n\n    try {\n      matchMedia.addEventListener('change', handleChange);\n      /* c8 ignore next 4 */\n    } catch {\n      // Safari isn't supporting matchMedia.addEventListener\n      matchMedia.addListener(handleChange);\n    }\n\n    return () => {\n      try {\n        matchMedia.removeEventListener('change', handleChange);\n        /* c8 ignore next 4 */\n      } catch {\n        // Safari isn't supporting matchMedia.removeEventListener\n        matchMedia.removeListener(handleChange);\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [input]);\n\n  return matches;\n}\n\nexport function usePrevious<T>(value: T): T {\n  const ref: any = useRef<T>();\n\n  useEffect(() => {\n    ref.current = value;\n  }, [value]);\n\n  return ref.current;\n}\n"
  },
  {
    "path": "src/modules/spotify.ts",
    "content": "/* eslint-disable camelcase */\nimport { parseIds } from '~/modules/helpers';\n\nimport { IDs, RepeatState, SpotifyPlayOptions } from '~/types';\n\nexport async function checkTracksStatus(token: string, tracks: IDs): Promise<boolean[]> {\n  return fetch(`https://api.spotify.com/v1/me/tracks/contains?ids=${parseIds(tracks)}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function getAlbumTracks(\n  token: string,\n  id: string,\n): Promise<SpotifyApi.AlbumTracksResponse> {\n  return fetch(`https://api.spotify.com/v1/albums/${id}/tracks`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function getArtistTopTracks(\n  token: string,\n  id: string,\n): Promise<SpotifyApi.ArtistsTopTracksResponse> {\n  return fetch(`https://api.spotify.com/v1/artists/${id}/top-tracks`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function getDevices(token: string): Promise<SpotifyApi.UserDevicesResponse> {\n  return fetch(`https://api.spotify.com/v1/me/player/devices`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function getPlaybackState(\n  token: string,\n): Promise<SpotifyApi.CurrentlyPlayingObject | null> {\n  return fetch(`https://api.spotify.com/v1/me/player`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => {\n    if (d.status === 204) {\n      return null;\n    }\n\n    return d.json();\n  });\n}\n\nexport async function getPlaylistTracks(\n  token: string,\n  id: string,\n): Promise<SpotifyApi.PlaylistTrackResponse> {\n  return fetch(`https://api.spotify.com/v1/playlists/${id}/tracks`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function getQueue(token: string): Promise<SpotifyApi.UsersQueueResponse> {\n  return fetch(`https://api.spotify.com/v1/me/player/queue`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function getShow(token: string, id: string): Promise<SpotifyApi.ShowObjectFull> {\n  return fetch(`https://api.spotify.com/v1/shows/${id}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function getShowEpisodes(\n  token: string,\n  id: string,\n  offset = 0,\n): Promise<SpotifyApi.ShowEpisodesResponse> {\n  return fetch(`https://api.spotify.com/v1/shows/${id}/episodes?offset=${offset}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function getTrack(token: string, id: string): Promise<SpotifyApi.TrackObjectFull> {\n  return fetch(`https://api.spotify.com/v1/tracks/${id}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'GET',\n  }).then(d => d.json());\n}\n\nexport async function next(token: string, deviceId?: string): Promise<void> {\n  let query = '';\n\n  if (deviceId) {\n    query += `?device_id=${deviceId}`;\n  }\n\n  await fetch(`https://api.spotify.com/v1/me/player/next${query}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'POST',\n  });\n}\n\nexport async function pause(token: string, deviceId?: string): Promise<void> {\n  let query = '';\n\n  if (deviceId) {\n    query += `?device_id=${deviceId}`;\n  }\n\n  await fetch(`https://api.spotify.com/v1/me/player/pause${query}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'PUT',\n  });\n}\n\nexport async function play(\n  token: string,\n  { context_uri, deviceId, offset = 0, uris }: SpotifyPlayOptions,\n): Promise<void> {\n  let body;\n\n  if (context_uri) {\n    const isArtist = context_uri.indexOf('artist') >= 0;\n    let position;\n\n    if (!isArtist) {\n      position = { position: offset };\n    }\n\n    body = JSON.stringify({ context_uri, offset: position });\n  } else if (Array.isArray(uris) && uris.length) {\n    body = JSON.stringify({ uris, offset: { position: offset } });\n  }\n\n  await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`, {\n    body,\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'PUT',\n  });\n}\n\nexport async function previous(token: string, deviceId?: string): Promise<void> {\n  let query = '';\n\n  if (deviceId) {\n    query += `?device_id=${deviceId}`;\n  }\n\n  await fetch(`https://api.spotify.com/v1/me/player/previous${query}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'POST',\n  });\n}\n\nexport async function removeTracks(token: string, tracks: IDs): Promise<void> {\n  await fetch(`https://api.spotify.com/v1/me/tracks`, {\n    body: JSON.stringify({ ids: parseIds(tracks) }),\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'DELETE',\n  });\n}\n\nexport async function repeat(token: string, state: RepeatState, deviceId?: string): Promise<void> {\n  let query = `?state=${state}`;\n\n  if (deviceId) {\n    query += `&device_id=${deviceId}`;\n  }\n\n  await fetch(`https://api.spotify.com/v1/me/player/repeat${query}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'PUT',\n  });\n}\n\nexport async function saveTracks(token: string, tracks: IDs): Promise<void> {\n  await fetch(`https://api.spotify.com/v1/me/tracks`, {\n    body: JSON.stringify({ ids: parseIds(tracks) }),\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'PUT',\n  });\n}\n\nexport async function seek(token: string, position: number, deviceId?: string): Promise<void> {\n  let query = `?position_ms=${position}`;\n\n  if (deviceId) {\n    query += `&device_id=${deviceId}`;\n  }\n\n  await fetch(`https://api.spotify.com/v1/me/player/seek${query}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'PUT',\n  });\n}\n\nexport async function setDevice(\n  token: string,\n  deviceId: string,\n  shouldPlay?: boolean,\n): Promise<void> {\n  await fetch(`https://api.spotify.com/v1/me/player`, {\n    body: JSON.stringify({ device_ids: [deviceId], play: shouldPlay }),\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'PUT',\n  });\n}\n\nexport async function setVolume(token: string, volume: number, deviceId?: string): Promise<void> {\n  let query = `?volume_percent=${volume}`;\n\n  if (deviceId) {\n    query += `&device_id=${deviceId}`;\n  }\n\n  await fetch(`https://api.spotify.com/v1/me/player/volume${query}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'PUT',\n  });\n}\n\nexport async function shuffle(token: string, state: boolean, deviceId?: string): Promise<void> {\n  let query = `?state=${state}`;\n\n  if (deviceId) {\n    query += `&device_id=${deviceId}`;\n  }\n\n  await fetch(`https://api.spotify.com/v1/me/player/shuffle${query}`, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    method: 'PUT',\n  });\n}\n"
  },
  {
    "path": "src/modules/styled.tsx",
    "content": "/* eslint-disable import/extensions */\n/* tslint:disable:object-literal-sort-keys */\nimport { createElement, FunctionComponent } from 'react';\nimport { create, CssLikeObject, NanoRenderer } from 'nano-css';\n// @ts-ignore\nimport { addon as addonJSX } from 'nano-css/addon/jsx.js';\nimport { addon as addonKeyframes } from 'nano-css/addon/keyframes.js';\n// @ts-ignore\nimport { addon as addonNesting } from 'nano-css/addon/nesting.js';\nimport { addon as addonRule } from 'nano-css/addon/rule.js';\n// @ts-ignore\nimport { addon as addonStyle } from 'nano-css/addon/style.js';\n// @ts-ignore\nimport { addon as addonStyled } from 'nano-css/addon/styled.js';\n\nimport { StyledProps } from '~/types';\n\ninterface NanoExtended extends NanoRenderer {\n  styled: (\n    tag: string,\n  ) => (\n    styles: CssLikeObject,\n    dynamicTemplate?: (props: StyledProps) => CssLikeObject,\n    block?: string,\n  ) => FunctionComponent<Partial<StyledProps>>;\n}\n\nconst nano = create({ h: createElement });\n\naddonRule(nano);\naddonKeyframes(nano);\naddonJSX(nano);\naddonStyle(nano);\naddonStyled(nano);\naddonNesting(nano);\n\nconst { keyframes, put, styled } = nano as NanoExtended;\n\nexport const px = (value: string | number): string =>\n  typeof value === 'number' ? `${value}px` : value;\n\nexport { keyframes, put, styled };\n\nexport { type CssLikeObject } from 'nano-css';\n"
  },
  {
    "path": "src/types/common.ts",
    "content": "import { ReactNode } from 'react';\n\nimport { ERROR_TYPE, STATUS, TYPE } from '~/constants';\n\nimport { SpotifyDevice, SpotifyTrack } from './spotify';\n\nexport type ErrorType = (typeof ERROR_TYPE)[keyof typeof ERROR_TYPE];\nexport type IDs = string | string[];\nexport type Layout = 'responsive' | 'compact';\nexport type RepeatState = 'off' | 'context' | 'track';\nexport type Status = (typeof STATUS)[keyof typeof STATUS];\nexport type StylesProps = Partial<StylesOptions>;\n\nexport type Type = (typeof TYPE)[keyof typeof TYPE];\n\nexport interface CallbackState extends State {\n  type: Type;\n}\n\nexport interface ComponentsProps {\n  [key: string]: any;\n  children?: ReactNode;\n  styles: StylesOptions;\n}\n\nexport interface CustomComponents {\n  /**\n   * A React component to be displayed before the previous button.\n   */\n  leftButton?: ReactNode;\n  /**\n   * A React component to be displayed after the next button.\n   */\n  rightButton?: ReactNode;\n}\n\nexport interface Locale {\n  currentDevice: string;\n  devices: string;\n  next: string;\n  otherDevices: string;\n  pause: string;\n  play: string;\n  previous: string;\n  removeTrack: string;\n  saveTrack: string;\n  title: string;\n  volume: string;\n}\n\nexport interface PlayOptions {\n  context_uri?: string;\n  uris?: string[];\n}\n\nexport interface Props {\n  /**\n   * Start the player immediately.\n   * @default false\n   * @deprecated Most browsers block autoplaying since the user needs to interact with the page first.\n   */\n  autoPlay?: boolean;\n  /**\n   * Get status updates from the player.\n   */\n  callback?: (state: CallbackState) => any;\n  /**\n   * Custom components for the player.\n   */\n  components?: CustomComponents;\n  /**\n   * The callback Spotify SDK uses to get/update the token.\n   */\n  getOAuthToken?: (callback: (token: string) => void) => Promise<void>;\n  /**\n   * Get the Spotify Web Playback SDK instance.\n   */\n  getPlayer?: (player: Spotify.Player) => void;\n  /**\n   * Hide the Spotify logo.\n   * More info: https://developer.spotify.com/documentation/general/design-and-branding/\n   * @default false\n   */\n  hideAttribution?: boolean;\n  /**\n   * Hide the cover art.\n   * @default false\n   */\n  hideCoverArt?: boolean;\n  /**\n   * The initial volume for the player. This isn't used for external devices.\n   * @default 1\n   */\n  initialVolume?: number;\n  /**\n   * Show the volume inline for the \"responsive\" layout for 768px and above.\n   * @default true\n   */\n  inlineVolume?: boolean;\n  /**\n   * The layout of the player.\n   * @default responsive\n   */\n  layout?: Layout;\n  /**\n   * The strings used for aria-label/title attributes.\n   */\n  locale?: Partial<Locale>;\n  /**\n   * Magnify the player's slider on hover.\n   * @default false\n   */\n  magnifySliderOnHover?: boolean;\n  /**\n   * The name of the player.\n   * @default Spotify Web Player\n   */\n  name?: string;\n  /**\n   * The position of the list/tracks you want to start the player.\n   */\n  offset?: number;\n  /**\n   * Save the device selection.\n   * @default false\n   */\n  persistDeviceSelection?: boolean;\n  /**\n   * Control the player's status.\n   */\n  play?: boolean;\n  /**\n   * Preload the track data before playing.\n   */\n  preloadData?: boolean;\n  /**\n   * Display a Favorite button. It needs additional scopes in your token.\n   * @default false\n   */\n  showSaveIcon?: boolean;\n  /**\n   * Customize the player's appearance.\n   */\n  styles?: StylesProps;\n  /**\n   * If there are no URIs and an external device is playing, use the external player context.\n   *  @default false\n   */\n  syncExternalDevice?: boolean;\n  /**\n   * The time in seconds that the player will sync with external devices.\n   * @default 5\n   */\n  syncExternalDeviceInterval?: number;\n  /**\n   * A Spotify token.\n   */\n  token: string;\n  /**\n   * Provide you with a function to sync the track saved status in the player.\n   * This works in addition to the showSaveIcon prop, and it is only needed if you keep the track's saved status in your app.\n   */\n  updateSavedStatus?: (fn: (status: boolean) => any) => any;\n  /**\n   * A list of Spotify URIs.\n   */\n  uris: string | string[];\n}\n\nexport interface State {\n  currentDeviceId: string;\n  currentURI: string;\n  deviceId: string;\n  devices: SpotifyDevice[];\n  error: string;\n  errorType: ErrorType | null;\n  isActive: boolean;\n  isInitializing: boolean;\n  isMagnified: boolean;\n  isPlaying: boolean;\n  isSaved: boolean;\n  isUnsupported: boolean;\n  needsUpdate: boolean;\n  nextTracks: SpotifyTrack[];\n  playerPosition: 'bottom' | 'top';\n  position: number;\n  previousTracks: SpotifyTrack[];\n  progressMs: number;\n  repeat: RepeatState;\n  shuffle: boolean;\n  status: Status;\n  track: SpotifyTrack;\n  volume: number;\n}\n\nexport interface StyledProps {\n  [key: string]: any;\n  style: Record<string, any>;\n}\n\nexport interface StylesOptions {\n  activeColor: string;\n  bgColor: string;\n  color: string;\n  errorColor: string;\n  height: number;\n  loaderColor: string;\n  loaderSize: number | string;\n  sliderColor: string;\n  sliderHandleBorderRadius: number | string;\n  sliderHandleColor: string;\n  sliderHeight: number;\n  sliderTrackBorderRadius: number | string;\n  sliderTrackColor: string;\n  trackArtistColor: string;\n  trackNameColor: string;\n}\n"
  },
  {
    "path": "src/types/index.ts",
    "content": "export * from './common';\nexport * from './spotify';\n"
  },
  {
    "path": "src/types/spotify.ts",
    "content": "export type SpotifyAlbum = Spotify.Album;\n\nexport type SpotifyArtist = SpotifyApi.ArtistObjectSimplified;\n\nexport type SpotifyDevice = SpotifyApi.UserDevice;\n\nexport type SpotifyPlayerCallback = (token: string) => void;\n\nexport interface SpotifyPlayOptions {\n  context_uri?: string;\n  deviceId: string;\n  offset?: number;\n  uris?: string[];\n}\n\nexport interface SpotifyTrack {\n  artists: Pick<SpotifyArtist, 'name' | 'uri'>[];\n  durationMs: number;\n  id: string;\n  image: string;\n  name: string;\n  uri: string;\n}\n\nexport interface WebPlaybackArtist {\n  name: string;\n  uri: string;\n}\n"
  },
  {
    "path": "test/__setup__/global.d.ts",
    "content": "import 'jest-extended';\nimport 'vitest/globals';\n"
  },
  {
    "path": "test/__setup__/vitest.setup.ts",
    "content": "import '@testing-library/jest-dom';\n\nimport { configure } from '@testing-library/react';\nimport * as matchers from 'jest-extended';\nimport { vi } from 'vitest';\nimport createFetchMock from 'vitest-fetch-mock';\n\nconfigure({ testIdAttribute: 'data-component-name' });\n\nexpect.extend(matchers);\n\nconst fetchMock = createFetchMock(vi);\n\nfetchMock.enableMocks();\n\nObject.defineProperty(window, 'matchMedia', {\n  writable: true,\n  value: (query: string) => ({\n    matches: true,\n    media: query,\n    onchange: null,\n    addListener: vi.fn(), // deprecated\n    removeListener: vi.fn(), // deprecated\n    addEventListener: vi.fn(),\n    removeEventListener: vi.fn(),\n    dispatchEvent: vi.fn(),\n  }),\n});\n"
  },
  {
    "path": "test/__snapshots__/constants.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`ERROR_TYPE > should have all options 1`] = `\n{\n  \"ACCOUNT\": \"account\",\n  \"AUTHENTICATION\": \"authentication\",\n  \"INITIALIZATION\": \"initialization\",\n  \"PLAYBACK\": \"playback\",\n  \"PLAYER\": \"player\",\n}\n`;\n\nexports[`SPOTIFY_CONTENT_TYPE > should have all options 1`] = `\n{\n  \"ALBUM\": \"album\",\n  \"ARTIST\": \"artist\",\n  \"PLAYLIST\": \"playlist\",\n  \"SHOW\": \"show\",\n  \"TRACK\": \"track\",\n}\n`;\n\nexports[`STATUS > should have all options 1`] = `\n{\n  \"ERROR\": \"ERROR\",\n  \"IDLE\": \"IDLE\",\n  \"INITIALIZING\": \"INITIALIZING\",\n  \"READY\": \"READY\",\n  \"RUNNING\": \"RUNNING\",\n  \"UNSUPPORTED\": \"UNSUPPORTED\",\n}\n`;\n\nexports[`TYPE > should have all options 1`] = `\n{\n  \"DEVICE\": \"device_update\",\n  \"FAVORITE\": \"favorite_update\",\n  \"PLAYER\": \"player_update\",\n  \"PRELOAD\": \"preload_update\",\n  \"PROGRESS\": \"progress_update\",\n  \"STATUS\": \"status_update\",\n  \"TRACK\": \"track_update\",\n}\n`;\n"
  },
  {
    "path": "test/__snapshots__/index.spec.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`SpotifyWebPlayer > Device listeners > should handle \\`player_state_changed\\` 1`] = `\n<div\n  class=\"PlayerRSWP\"\n  data-component-name=\"Player\"\n  data-ready=\"true\"\n  style=\"background: rgb(255, 255, 255); min-height: 80px;\"\n>\n  <div\n    class=\" _WrapperRSWP __1g3et1y\"\n    data-component-name=\"Wrapper\"\n  >\n    <div\n      class=\"rswp__active _InfoRSWP __14asrht\"\n      data-component-name=\"Info\"\n    >\n      <a\n        aria-label=\"Main Theme From Trouble Man on SPOTIFY\"\n        href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n        rel=\"noreferrer\"\n        target=\"_blank\"\n        title=\"Main Theme From Trouble Man on SPOTIFY\"\n      >\n        <img\n          alt=\"Main Theme From Trouble Man\"\n          src=\"https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052\"\n        />\n      </a>\n      <div\n        class=\" _ContentWrapperRSWP __1c53jt9\"\n      >\n        <div\n          class=\" _ContentRSWP __1of0817\"\n        >\n          <div\n            data-type=\"title-artist-wrapper\"\n          >\n            <div>\n              <p>\n                <span>\n                  <a\n                    aria-label=\"Main Theme From Trouble Man on SPOTIFY\"\n                    href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    title=\"Main Theme From Trouble Man on SPOTIFY\"\n                  >\n                    Main Theme From Trouble Man\n                  </a>\n                </span>\n              </p>\n              <p\n                title=\"Marvin Gaye\"\n              >\n                <span>\n                  <a\n                    aria-label=\"Marvin Gaye on SPOTIFY\"\n                    href=\"https://open.spotify.com/artist/3koiLjNrgRTNbOwViDipeA\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    title=\"Marvin Gaye on SPOTIFY\"\n                  >\n                    Marvin Gaye\n                  </a>\n                </span>\n              </p>\n            </div>\n          </div>\n        </div>\n        <a\n          aria-label=\"Play on Spotify\"\n          href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n          rel=\"noreferrer\"\n          target=\"_blank\"\n        >\n          <svg\n            height=\"1em\"\n            preserveAspectRatio=\"xMidYMid\"\n            viewBox=\"0 0 512 160\"\n            width=\"3.2em\"\n          >\n            <path\n              d=\"M79.655 0C35.664 0 0 35.663 0 79.654c0 43.993 35.664 79.653 79.655 79.653 43.996 0 79.656-35.66 79.656-79.653 0-43.988-35.66-79.65-79.657-79.65L79.655 0Zm36.53 114.884a4.963 4.963 0 0 1-6.83 1.646c-18.702-11.424-42.246-14.011-69.973-7.676a4.967 4.967 0 0 1-5.944-3.738 4.958 4.958 0 0 1 3.734-5.945c30.343-6.933 56.37-3.948 77.367 8.884a4.965 4.965 0 0 1 1.645 6.83Zm9.75-21.689c-1.799 2.922-5.622 3.845-8.543 2.047-21.41-13.16-54.049-16.972-79.374-9.284a6.219 6.219 0 0 1-7.75-4.138 6.22 6.22 0 0 1 4.141-7.745c28.929-8.778 64.892-4.526 89.48 10.583 2.92 1.798 3.843 5.622 2.045 8.538Zm.836-22.585C101.1 55.362 58.742 53.96 34.231 61.4c-3.936 1.194-8.098-1.028-9.29-4.964a7.453 7.453 0 0 1 4.965-9.294c28.137-8.542 74.912-6.892 104.469 10.655a7.441 7.441 0 0 1 2.606 10.209c-2.092 3.54-6.677 4.707-10.206 2.605h-.004Zm89.944 2.922c-13.754-3.28-16.198-5.581-16.198-10.418 0-4.57 4.299-7.645 10.7-7.645 6.202 0 12.347 2.336 18.796 7.143.19.145.437.203.675.165a.888.888 0 0 0 .6-.367l6.715-9.466a.903.903 0 0 0-.171-1.225c-7.676-6.157-16.313-9.15-26.415-9.15-14.848 0-25.225 8.911-25.225 21.662 0 13.673 8.95 18.515 24.417 22.252 13.155 3.031 15.38 5.57 15.38 10.11 0 5.032-4.49 8.161-11.718 8.161-8.028 0-14.582-2.71-21.906-9.046a.932.932 0 0 0-.656-.218.89.89 0 0 0-.619.313l-7.533 8.96a.906.906 0 0 0 .086 1.256c8.522 7.61 19.004 11.624 30.323 11.624 16 0 26.339-8.742 26.339-22.277.028-11.421-6.81-17.746-23.561-21.821l-.029-.013Zm59.792-13.564c-6.934 0-12.622 2.732-17.321 8.33v-6.3c0-.498-.4-.903-.894-.903h-12.318a.899.899 0 0 0-.894.902v70.009c0 .494.4.903.894.903h12.318a.901.901 0 0 0 .894-.903v-22.097c4.699 5.26 10.387 7.838 17.32 7.838 12.89 0 25.94-9.92 25.94-28.886.019-18.97-13.032-28.894-25.93-28.894l-.01.001Zm11.614 28.893c0 9.653-5.945 16.397-14.468 16.397-8.418 0-14.772-7.048-14.772-16.397 0-9.35 6.354-16.397 14.772-16.397 8.38 0 14.468 6.893 14.468 16.396Zm47.759-28.893c-16.598 0-29.601 12.78-29.601 29.1 0 16.143 12.917 28.784 29.401 28.784 16.655 0 29.696-12.736 29.696-28.991 0-16.2-12.955-28.89-29.496-28.89v-.003Zm0 45.385c-8.827 0-15.485-7.096-15.485-16.497 0-9.444 6.43-16.298 15.285-16.298 8.884 0 15.58 7.093 15.58 16.504 0 9.443-6.468 16.291-15.38 16.291Zm64.937-44.258h-13.554V47.24c0-.497-.4-.902-.894-.902H374.05a.906.906 0 0 0-.904.902v13.855h-5.916a.899.899 0 0 0-.894.902v10.584a.9.9 0 0 0 .894.903h5.916v27.39c0 11.062 5.508 16.674 16.38 16.674 4.413 0 8.075-.914 11.528-2.873a.88.88 0 0 0 .457-.78v-10.083a.896.896 0 0 0-.428-.76.873.873 0 0 0-.876-.039c-2.368 1.19-4.66 1.741-7.229 1.741-3.947 0-5.716-1.798-5.716-5.812V73.49h13.554a.899.899 0 0 0 .894-.903V62.003a.873.873 0 0 0-.884-.903l-.01-.005Zm47.217.054v-1.702c0-5.006 1.921-7.238 6.22-7.238 2.57 0 4.633.51 6.945 1.28a.895.895 0 0 0 1.18-.858l-.001-10.377a.891.891 0 0 0-.637-.865c-2.435-.726-5.555-1.47-10.235-1.47-11.367 0-17.388 6.405-17.388 18.516v2.606h-5.916a.906.906 0 0 0-.904.902v10.638c0 .497.41.903.904.903h5.916v42.237c0 .504.41.904.904.904h12.308c.504 0 .904-.4.904-.904V73.487h11.5l17.616 42.234c-1.998 4.433-3.967 5.317-6.65 5.317-2.168 0-4.46-.646-6.79-1.93a.98.98 0 0 0-.714-.067.896.896 0 0 0-.533.485l-4.175 9.16a.9.9 0 0 0 .39 1.17c4.356 2.359 8.284 3.367 13.145 3.367 9.093 0 14.125-4.242 18.548-15.637l21.364-55.204a.88.88 0 0 0-.095-.838.878.878 0 0 0-.733-.392h-12.822a.901.901 0 0 0-.856.605l-13.136 37.509-14.382-37.534a.898.898 0 0 0-.837-.58h-21.04v-.003Zm-27.375-.054h-12.318a.907.907 0 0 0-.903.902v53.724c0 .504.409.904.903.904h12.318c.495 0 .904-.4.904-.904v-53.72a.9.9 0 0 0-.904-.903v-.003Zm-6.088-24.464c-4.88 0-8.836 3.95-8.836 8.828a8.835 8.835 0 0 0 8.836 8.836c4.88 0 8.827-3.954 8.827-8.836a8.83 8.83 0 0 0-8.827-8.828Z\"\n              fill=\"#000000\"\n            />\n          </svg>\n        </a>\n      </div>\n    </div>\n    <div\n      class=\" _ControlsRSWP __11aa6ep\"\n      data-component-name=\"Controls\"\n      data-playing=\"true\"\n    >\n      <div\n        class=\" _ControlsButtonsRSWP __a3qe0k\"\n      >\n        <div\n          class=\"rswp__devices\"\n        >\n          <div>\n            <div\n              class=\" _DevicesRSWP __n5xkwq\"\n              data-component-name=\"Devices\"\n              data-device-id=\"19ks98hfbxc53vh34jd\"\n            >\n              <button\n                aria-label=\"Devices\"\n                class=\"ButtonRSWP\"\n                title=\"Devices\"\n                type=\"button\"\n              >\n                <svg\n                  height=\"1em\"\n                  preserveAspectRatio=\"xMidYMid\"\n                  viewBox=\"0 0 64 64\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M57 4c3.864 0 7 3.136 7 7v42a7 7 0 0 1-7 7H31a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h26ZM16 54v6H8v-6h8Zm41-44H31a1 1 0 0 0-1 1v42a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1ZM44 32a8 8 0 1 1 0 16 8 8 0 0 1 0-16ZM16 4v6H7a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h9v6H7a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h9Zm28 12a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n                    fill=\"currentColor\"\n                  />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n        <div />\n        <div>\n          <button\n            aria-label=\"Previous\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Previous\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M10.514 0a3.2 3.2 0 0 1 3.2 3.2v23.543L59.2.489A3.2 3.2 0 0 1 64 3.255V60.74a3.2 3.2 0 0 1-4.8 2.774L13.714 37.253V60.8a3.2 3.2 0 0 1-3.2 3.2H3.2A3.2 3.2 0 0 1 0 60.8V3.2A3.2 3.2 0 0 1 3.2 0h7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Pause\"\n            class=\"ButtonRSWP rswp__toggle _ControlsButtonRSWP __3hmsj\"\n            title=\"Pause\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-5.4 18h-5.2a1.4 1.4 0 0 0-1.4 1.4v25.2a1.4 1.4 0 0 0 1.4 1.4h5.2a1.4 1.4 0 0 0 1.4-1.4V19.4a1.4 1.4 0 0 0-1.4-1.4Zm16 0h-5.2a1.4 1.4 0 0 0-1.4 1.4v25.2a1.4 1.4 0 0 0 1.4 1.4h5.2a1.4 1.4 0 0 0 1.4-1.4V19.4a1.4 1.4 0 0 0-1.4-1.4Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Next\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Next\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M53.486 0a3.2 3.2 0 0 0-3.2 3.2v23.543L4.8.489A3.2 3.2 0 0 0 0 3.255V60.74a3.2 3.2 0 0 0 4.8 2.774l45.486-26.262V60.8a3.2 3.2 0 0 0 3.2 3.2H60.8a3.2 3.2 0 0 0 3.2-3.2V3.2A3.2 3.2 0 0 0 60.8 0h-7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div />\n        <div\n          class=\"rswp__volume\"\n        >\n          <div\n            class=\" _VolumeInlineRSWP __a3qe0k\"\n            data-component-name=\"Volume\"\n            data-value=\"1\"\n          >\n            <span>\n              <svg\n                data-component-name=\"VolumeHigh\"\n                height=\"1em\"\n                preserveAspectRatio=\"xMidYMid\"\n                viewBox=\"0 0 64 64\"\n                width=\"1em\"\n              >\n                <path\n                  d=\"M37.963 3.402a2.989 2.989 0 0 1 1.5 2.596v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0ZM45 9.542a23.008 23.008 0 0 1 0 44.912V48.25a17.008 17.008 0 0 0 0-32.508Zm-11.532 1.656-23.2 13.4a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6ZM45 22.238a11 11 0 0 1 0 19.52v-19.52Z\"\n                  fill=\"currentColor\"\n                />\n              </svg>\n            </span>\n            <div>\n              <div\n                class=\"volume\"\n                data-component-name=\"volume-bar\"\n                style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n              >\n                <div\n                  class=\"volume__track\"\n                  role=\"presentation\"\n                  style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n                >\n                  <div\n                    class=\"volume__range\"\n                    style=\"width: 100%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n                  />\n                  <div\n                    role=\"presentation\"\n                    style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 100%; bottom: 0%;\"\n                  >\n                    <span\n                      aria-label=\"slider handle\"\n                      aria-orientation=\"horizontal\"\n                      aria-valuemax=\"100\"\n                      aria-valuemin=\"0\"\n                      aria-valuenow=\"100\"\n                      class=\"volume__thumb\"\n                      role=\"slider\"\n                      style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                      tabindex=\"0\"\n                    />\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div\n        class=\" _SliderRSWP __q14q70\"\n        data-component-name=\"Slider\"\n        data-position=\"0\"\n      >\n        <div\n          class=\"rswp_progress\"\n        >\n          0:00\n        </div>\n        <div\n          class=\"slider\"\n          data-component-name=\"progress-bar\"\n          style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n        >\n          <div\n            class=\"slider__track\"\n            role=\"presentation\"\n            style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n          >\n            <div\n              class=\"slider__range\"\n              style=\"width: 0%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n            />\n            <div\n              role=\"presentation\"\n              style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 0%; bottom: 0%;\"\n            >\n              <span\n                aria-label=\"slider handle\"\n                aria-orientation=\"horizontal\"\n                aria-valuemax=\"100\"\n                aria-valuemin=\"0\"\n                aria-valuenow=\"0\"\n                class=\"slider__thumb\"\n                role=\"slider\"\n                style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                tabindex=\"0\"\n              />\n            </div>\n          </div>\n        </div>\n        <div\n          class=\"rswp_duration\"\n        >\n          2:31\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`SpotifyWebPlayer > Device listeners > should handle \\`ready\\` 1`] = `\n<div\n  class=\"PlayerRSWP\"\n  data-component-name=\"Player\"\n  data-ready=\"true\"\n  style=\"background: rgb(255, 255, 255); min-height: 80px;\"\n>\n  <div\n    class=\" _WrapperRSWP __1g3et1y\"\n    data-component-name=\"Wrapper\"\n  >\n    <div />\n    <div\n      class=\" _ControlsRSWP __11aa6ep\"\n      data-component-name=\"Controls\"\n      data-playing=\"false\"\n    >\n      <div\n        class=\" _ControlsButtonsRSWP __a3qe0k\"\n      >\n        <div\n          class=\"rswp__devices\"\n        >\n          <div>\n            <div\n              class=\" _DevicesRSWP __n5xkwq\"\n              data-component-name=\"Devices\"\n              data-device-id=\"19ks98hfbxc53vh34jd\"\n            >\n              <button\n                aria-label=\"Devices\"\n                class=\"ButtonRSWP\"\n                title=\"Devices\"\n                type=\"button\"\n              >\n                <svg\n                  height=\"1em\"\n                  preserveAspectRatio=\"xMidYMid\"\n                  viewBox=\"0 0 64 64\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M57 4c3.864 0 7 3.136 7 7v42a7 7 0 0 1-7 7H31a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h26ZM16 54v6H8v-6h8Zm41-44H31a1 1 0 0 0-1 1v42a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1ZM44 32a8 8 0 1 1 0 16 8 8 0 0 1 0-16ZM16 4v6H7a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h9v6H7a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h9Zm28 12a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n                    fill=\"currentColor\"\n                  />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n        <div />\n        <div>\n          <button\n            aria-label=\"Previous\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            disabled=\"\"\n            title=\"Previous\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M10.514 0a3.2 3.2 0 0 1 3.2 3.2v23.543L59.2.489A3.2 3.2 0 0 1 64 3.255V60.74a3.2 3.2 0 0 1-4.8 2.774L13.714 37.253V60.8a3.2 3.2 0 0 1-3.2 3.2H3.2A3.2 3.2 0 0 1 0 60.8V3.2A3.2 3.2 0 0 1 3.2 0h7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Play\"\n            class=\"ButtonRSWP rswp__toggle _ControlsButtonRSWP __3hmsj\"\n            title=\"Play\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-7.61 18.188c-.435.251-.702.715-.701 1.216v25.194a1.402 1.402 0 0 0 2.104 1.214L47.61 33.214a1.402 1.402 0 0 0 0-2.428L25.793 18.188c-.435-.25-.97-.25-1.404 0Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Next\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            disabled=\"\"\n            title=\"Next\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M53.486 0a3.2 3.2 0 0 0-3.2 3.2v23.543L4.8.489A3.2 3.2 0 0 0 0 3.255V60.74a3.2 3.2 0 0 0 4.8 2.774l45.486-26.262V60.8a3.2 3.2 0 0 0 3.2 3.2H60.8a3.2 3.2 0 0 0 3.2-3.2V3.2A3.2 3.2 0 0 0 60.8 0h-7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div />\n        <div\n          class=\"rswp__volume\"\n        >\n          <div\n            class=\" _VolumeInlineRSWP __a3qe0k\"\n            data-component-name=\"Volume\"\n            data-value=\"1\"\n          >\n            <span>\n              <svg\n                data-component-name=\"VolumeHigh\"\n                height=\"1em\"\n                preserveAspectRatio=\"xMidYMid\"\n                viewBox=\"0 0 64 64\"\n                width=\"1em\"\n              >\n                <path\n                  d=\"M37.963 3.402a2.989 2.989 0 0 1 1.5 2.596v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0ZM45 9.542a23.008 23.008 0 0 1 0 44.912V48.25a17.008 17.008 0 0 0 0-32.508Zm-11.532 1.656-23.2 13.4a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6ZM45 22.238a11 11 0 0 1 0 19.52v-19.52Z\"\n                  fill=\"currentColor\"\n                />\n              </svg>\n            </span>\n            <div>\n              <div\n                class=\"volume\"\n                data-component-name=\"volume-bar\"\n                style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n              >\n                <div\n                  class=\"volume__track\"\n                  role=\"presentation\"\n                  style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n                >\n                  <div\n                    class=\"volume__range\"\n                    style=\"width: 100%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n                  />\n                  <div\n                    role=\"presentation\"\n                    style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 100%; bottom: 0%;\"\n                  >\n                    <span\n                      aria-label=\"slider handle\"\n                      aria-orientation=\"horizontal\"\n                      aria-valuemax=\"100\"\n                      aria-valuemin=\"0\"\n                      aria-valuenow=\"100\"\n                      class=\"volume__thumb\"\n                      role=\"slider\"\n                      style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                      tabindex=\"0\"\n                    />\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div\n        class=\" _SliderRSWP __q14q70\"\n        data-component-name=\"Slider\"\n        data-position=\"0\"\n      >\n        <div\n          class=\"rswp_progress\"\n        >\n          0:00\n        </div>\n        <div\n          class=\"slider\"\n          data-component-name=\"progress-bar\"\n          style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n        >\n          <div\n            class=\"slider__track\"\n            role=\"presentation\"\n            style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n          >\n            <div\n              class=\"slider__range\"\n              style=\"width: 0%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n            />\n            <div\n              role=\"presentation\"\n              style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 0%; bottom: 0%;\"\n            >\n              <span\n                aria-label=\"slider handle\"\n                aria-orientation=\"horizontal\"\n                aria-valuemax=\"100\"\n                aria-valuemin=\"0\"\n                aria-valuenow=\"0\"\n                class=\"slider__thumb\"\n                role=\"slider\"\n                style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                tabindex=\"0\"\n              />\n            </div>\n          </div>\n        </div>\n        <div\n          class=\"rswp_duration\"\n        >\n          0:00\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`SpotifyWebPlayer > Error listeners > should handle \\`account_error\\` 1`] = `\n<div\n  class=\"PlayerRSWP\"\n  data-component-name=\"Player\"\n  data-ready=\"false\"\n  style=\"background: rgb(255, 255, 255); min-height: 80px;\"\n>\n  <div\n    class=\" _ErrorRSWP __1rvrb20\"\n    data-component-name=\"ErrorMessage\"\n  >\n    Failed to validate Spotify account\n  </div>\n</div>\n`;\n\nexports[`SpotifyWebPlayer > Error listeners > should handle \\`authentication_error\\` > With Error 1`] = `\n<div\n  class=\"PlayerRSWP\"\n  data-component-name=\"Player\"\n  data-ready=\"false\"\n  style=\"background: rgb(255, 255, 255); min-height: 80px;\"\n>\n  <div\n    class=\" _ErrorRSWP __1rvrb20\"\n    data-component-name=\"ErrorMessage\"\n  >\n    Failed to authenticate\n  </div>\n</div>\n`;\n\nexports[`SpotifyWebPlayer > Error listeners > should handle \\`initialization_error\\` 1`] = `\n<div\n  class=\"PlayerRSWP\"\n  data-component-name=\"Player\"\n  data-ready=\"true\"\n  style=\"background: rgb(255, 255, 255); min-height: 80px;\"\n>\n  <div\n    class=\" _WrapperRSWP __1g3et1y\"\n    data-component-name=\"Wrapper\"\n  >\n    <div />\n    <div\n      class=\" _ControlsRSWP __11aa6ep\"\n      data-component-name=\"Controls\"\n      data-playing=\"false\"\n    >\n      <div\n        class=\" _ControlsButtonsRSWP __a3qe0k\"\n      >\n        <div\n          class=\"rswp__devices\"\n        >\n          <div>\n            <div\n              class=\" _DevicesRSWP __n5xkwq\"\n              data-component-name=\"Devices\"\n              data-device-id=\"\"\n            >\n              <div>\n                <p>\n                  Select other device\n                </p>\n                <button\n                  aria-label=\"Test Player\"\n                  class=\"ButtonRSWP\"\n                  data-id=\"df17372ghs982js892js\"\n                  type=\"button\"\n                >\n                  <svg\n                    height=\"1em\"\n                    preserveAspectRatio=\"xMidYMid\"\n                    viewBox=\"0 0 64 64\"\n                    width=\"1em\"\n                  >\n                    <path\n                      d=\"M7.226 10.323a7.228 7.228 0 0 1 7.226-7.226h35.096a7.228 7.228 0 0 1 7.226 7.226V37.16a7.226 7.226 0 0 1-7.226 7.226H14.452a7.226 7.226 0 0 1-7.226-7.226V10.323Zm7.226-1.033c-.57 0-1.033.462-1.033 1.033V37.16c0 .57.463 1.033 1.033 1.033h35.096c.57 0 1.033-.463 1.033-1.033V10.323c0-.57-.463-1.033-1.033-1.033H14.452ZM0 57.806a3.097 3.097 0 0 1 3.097-3.096h57.806a3.097 3.097 0 0 1 0 6.193H3.097A3.097 3.097 0 0 1 0 57.806Z\"\n                      fill=\"currentColor\"\n                    />\n                  </svg>\n                  <span>\n                    Test Player\n                  </span>\n                </button>\n                <span />\n              </div>\n              <button\n                aria-label=\"Devices\"\n                class=\"ButtonRSWP\"\n                title=\"Devices\"\n                type=\"button\"\n              >\n                <svg\n                  height=\"1em\"\n                  preserveAspectRatio=\"xMidYMid\"\n                  viewBox=\"0 0 64 64\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M57 4c3.864 0 7 3.136 7 7v42a7 7 0 0 1-7 7H31a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h26ZM16 54v6H8v-6h8Zm41-44H31a1 1 0 0 0-1 1v42a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1ZM44 32a8 8 0 1 1 0 16 8 8 0 0 1 0-16ZM16 4v6H7a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h9v6H7a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h9Zm28 12a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n                    fill=\"currentColor\"\n                  />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n        <div />\n        <div>\n          <button\n            aria-label=\"Previous\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Previous\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M10.514 0a3.2 3.2 0 0 1 3.2 3.2v23.543L59.2.489A3.2 3.2 0 0 1 64 3.255V60.74a3.2 3.2 0 0 1-4.8 2.774L13.714 37.253V60.8a3.2 3.2 0 0 1-3.2 3.2H3.2A3.2 3.2 0 0 1 0 60.8V3.2A3.2 3.2 0 0 1 3.2 0h7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Play\"\n            class=\"ButtonRSWP rswp__toggle _ControlsButtonRSWP __3hmsj\"\n            title=\"Play\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-7.61 18.188c-.435.251-.702.715-.701 1.216v25.194a1.402 1.402 0 0 0 2.104 1.214L47.61 33.214a1.402 1.402 0 0 0 0-2.428L25.793 18.188c-.435-.25-.97-.25-1.404 0Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Next\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Next\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M53.486 0a3.2 3.2 0 0 0-3.2 3.2v23.543L4.8.489A3.2 3.2 0 0 0 0 3.255V60.74a3.2 3.2 0 0 0 4.8 2.774l45.486-26.262V60.8a3.2 3.2 0 0 0 3.2 3.2H60.8a3.2 3.2 0 0 0 3.2-3.2V3.2A3.2 3.2 0 0 0 60.8 0h-7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div />\n      </div>\n      <div\n        class=\" _SliderRSWP __q14q70\"\n        data-component-name=\"Slider\"\n        data-position=\"0\"\n      >\n        <div\n          class=\"rswp_progress\"\n        >\n          0:00\n        </div>\n        <div\n          class=\"slider\"\n          data-component-name=\"progress-bar\"\n          style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n        >\n          <div\n            class=\"slider__track\"\n            role=\"presentation\"\n            style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n          >\n            <div\n              class=\"slider__range\"\n              style=\"width: 0%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n            />\n            <div\n              role=\"presentation\"\n              style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 0%; bottom: 0%;\"\n            >\n              <span\n                aria-label=\"slider handle\"\n                aria-orientation=\"horizontal\"\n                aria-valuemax=\"100\"\n                aria-valuemin=\"0\"\n                aria-valuenow=\"0\"\n                class=\"slider__thumb\"\n                role=\"slider\"\n                style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                tabindex=\"0\"\n              />\n            </div>\n          </div>\n        </div>\n        <div\n          class=\"rswp_duration\"\n        >\n          0:00\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`SpotifyWebPlayer > Error listeners > should handle \\`playback_error\\` 1`] = `\n<div\n  class=\"PlayerRSWP\"\n  data-component-name=\"Player\"\n  data-ready=\"true\"\n  style=\"background: rgb(255, 255, 255); min-height: 80px;\"\n>\n  <div\n    class=\" _WrapperRSWP __1g3et1y\"\n    data-component-name=\"Wrapper\"\n  >\n    <div\n      class=\"rswp__active _InfoRSWP __14asrht\"\n      data-component-name=\"Info\"\n    >\n      <a\n        aria-label=\"Main Theme From Trouble Man on SPOTIFY\"\n        href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n        rel=\"noreferrer\"\n        target=\"_blank\"\n        title=\"Main Theme From Trouble Man on SPOTIFY\"\n      >\n        <img\n          alt=\"Main Theme From Trouble Man\"\n          src=\"https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052\"\n        />\n      </a>\n      <div\n        class=\" _ContentWrapperRSWP __1c53jt9\"\n      >\n        <div\n          class=\" _ContentRSWP __1of0817\"\n        >\n          <div\n            data-type=\"title-artist-wrapper\"\n          >\n            <div>\n              <p>\n                <span>\n                  <a\n                    aria-label=\"Main Theme From Trouble Man on SPOTIFY\"\n                    href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    title=\"Main Theme From Trouble Man on SPOTIFY\"\n                  >\n                    Main Theme From Trouble Man\n                  </a>\n                </span>\n              </p>\n              <p\n                title=\"Marvin Gaye\"\n              >\n                <span>\n                  <a\n                    aria-label=\"Marvin Gaye on SPOTIFY\"\n                    href=\"https://open.spotify.com/artist/3koiLjNrgRTNbOwViDipeA\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    title=\"Marvin Gaye on SPOTIFY\"\n                  >\n                    Marvin Gaye\n                  </a>\n                </span>\n              </p>\n            </div>\n          </div>\n        </div>\n        <a\n          aria-label=\"Play on Spotify\"\n          href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n          rel=\"noreferrer\"\n          target=\"_blank\"\n        >\n          <svg\n            height=\"1em\"\n            preserveAspectRatio=\"xMidYMid\"\n            viewBox=\"0 0 512 160\"\n            width=\"3.2em\"\n          >\n            <path\n              d=\"M79.655 0C35.664 0 0 35.663 0 79.654c0 43.993 35.664 79.653 79.655 79.653 43.996 0 79.656-35.66 79.656-79.653 0-43.988-35.66-79.65-79.657-79.65L79.655 0Zm36.53 114.884a4.963 4.963 0 0 1-6.83 1.646c-18.702-11.424-42.246-14.011-69.973-7.676a4.967 4.967 0 0 1-5.944-3.738 4.958 4.958 0 0 1 3.734-5.945c30.343-6.933 56.37-3.948 77.367 8.884a4.965 4.965 0 0 1 1.645 6.83Zm9.75-21.689c-1.799 2.922-5.622 3.845-8.543 2.047-21.41-13.16-54.049-16.972-79.374-9.284a6.219 6.219 0 0 1-7.75-4.138 6.22 6.22 0 0 1 4.141-7.745c28.929-8.778 64.892-4.526 89.48 10.583 2.92 1.798 3.843 5.622 2.045 8.538Zm.836-22.585C101.1 55.362 58.742 53.96 34.231 61.4c-3.936 1.194-8.098-1.028-9.29-4.964a7.453 7.453 0 0 1 4.965-9.294c28.137-8.542 74.912-6.892 104.469 10.655a7.441 7.441 0 0 1 2.606 10.209c-2.092 3.54-6.677 4.707-10.206 2.605h-.004Zm89.944 2.922c-13.754-3.28-16.198-5.581-16.198-10.418 0-4.57 4.299-7.645 10.7-7.645 6.202 0 12.347 2.336 18.796 7.143.19.145.437.203.675.165a.888.888 0 0 0 .6-.367l6.715-9.466a.903.903 0 0 0-.171-1.225c-7.676-6.157-16.313-9.15-26.415-9.15-14.848 0-25.225 8.911-25.225 21.662 0 13.673 8.95 18.515 24.417 22.252 13.155 3.031 15.38 5.57 15.38 10.11 0 5.032-4.49 8.161-11.718 8.161-8.028 0-14.582-2.71-21.906-9.046a.932.932 0 0 0-.656-.218.89.89 0 0 0-.619.313l-7.533 8.96a.906.906 0 0 0 .086 1.256c8.522 7.61 19.004 11.624 30.323 11.624 16 0 26.339-8.742 26.339-22.277.028-11.421-6.81-17.746-23.561-21.821l-.029-.013Zm59.792-13.564c-6.934 0-12.622 2.732-17.321 8.33v-6.3c0-.498-.4-.903-.894-.903h-12.318a.899.899 0 0 0-.894.902v70.009c0 .494.4.903.894.903h12.318a.901.901 0 0 0 .894-.903v-22.097c4.699 5.26 10.387 7.838 17.32 7.838 12.89 0 25.94-9.92 25.94-28.886.019-18.97-13.032-28.894-25.93-28.894l-.01.001Zm11.614 28.893c0 9.653-5.945 16.397-14.468 16.397-8.418 0-14.772-7.048-14.772-16.397 0-9.35 6.354-16.397 14.772-16.397 8.38 0 14.468 6.893 14.468 16.396Zm47.759-28.893c-16.598 0-29.601 12.78-29.601 29.1 0 16.143 12.917 28.784 29.401 28.784 16.655 0 29.696-12.736 29.696-28.991 0-16.2-12.955-28.89-29.496-28.89v-.003Zm0 45.385c-8.827 0-15.485-7.096-15.485-16.497 0-9.444 6.43-16.298 15.285-16.298 8.884 0 15.58 7.093 15.58 16.504 0 9.443-6.468 16.291-15.38 16.291Zm64.937-44.258h-13.554V47.24c0-.497-.4-.902-.894-.902H374.05a.906.906 0 0 0-.904.902v13.855h-5.916a.899.899 0 0 0-.894.902v10.584a.9.9 0 0 0 .894.903h5.916v27.39c0 11.062 5.508 16.674 16.38 16.674 4.413 0 8.075-.914 11.528-2.873a.88.88 0 0 0 .457-.78v-10.083a.896.896 0 0 0-.428-.76.873.873 0 0 0-.876-.039c-2.368 1.19-4.66 1.741-7.229 1.741-3.947 0-5.716-1.798-5.716-5.812V73.49h13.554a.899.899 0 0 0 .894-.903V62.003a.873.873 0 0 0-.884-.903l-.01-.005Zm47.217.054v-1.702c0-5.006 1.921-7.238 6.22-7.238 2.57 0 4.633.51 6.945 1.28a.895.895 0 0 0 1.18-.858l-.001-10.377a.891.891 0 0 0-.637-.865c-2.435-.726-5.555-1.47-10.235-1.47-11.367 0-17.388 6.405-17.388 18.516v2.606h-5.916a.906.906 0 0 0-.904.902v10.638c0 .497.41.903.904.903h5.916v42.237c0 .504.41.904.904.904h12.308c.504 0 .904-.4.904-.904V73.487h11.5l17.616 42.234c-1.998 4.433-3.967 5.317-6.65 5.317-2.168 0-4.46-.646-6.79-1.93a.98.98 0 0 0-.714-.067.896.896 0 0 0-.533.485l-4.175 9.16a.9.9 0 0 0 .39 1.17c4.356 2.359 8.284 3.367 13.145 3.367 9.093 0 14.125-4.242 18.548-15.637l21.364-55.204a.88.88 0 0 0-.095-.838.878.878 0 0 0-.733-.392h-12.822a.901.901 0 0 0-.856.605l-13.136 37.509-14.382-37.534a.898.898 0 0 0-.837-.58h-21.04v-.003Zm-27.375-.054h-12.318a.907.907 0 0 0-.903.902v53.724c0 .504.409.904.903.904h12.318c.495 0 .904-.4.904-.904v-53.72a.9.9 0 0 0-.904-.903v-.003Zm-6.088-24.464c-4.88 0-8.836 3.95-8.836 8.828a8.835 8.835 0 0 0 8.836 8.836c4.88 0 8.827-3.954 8.827-8.836a8.83 8.83 0 0 0-8.827-8.828Z\"\n              fill=\"#000000\"\n            />\n          </svg>\n        </a>\n      </div>\n    </div>\n    <div\n      class=\" _ControlsRSWP __11aa6ep\"\n      data-component-name=\"Controls\"\n      data-playing=\"false\"\n    >\n      <div\n        class=\" _ControlsButtonsRSWP __a3qe0k\"\n      >\n        <div\n          class=\"rswp__devices\"\n        >\n          <div>\n            <div\n              class=\" _DevicesRSWP __n5xkwq\"\n              data-component-name=\"Devices\"\n              data-device-id=\"19ks98hfbxc53vh34jd\"\n            />\n          </div>\n        </div>\n        <div />\n        <div>\n          <button\n            aria-label=\"Previous\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Previous\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M10.514 0a3.2 3.2 0 0 1 3.2 3.2v23.543L59.2.489A3.2 3.2 0 0 1 64 3.255V60.74a3.2 3.2 0 0 1-4.8 2.774L13.714 37.253V60.8a3.2 3.2 0 0 1-3.2 3.2H3.2A3.2 3.2 0 0 1 0 60.8V3.2A3.2 3.2 0 0 1 3.2 0h7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Play\"\n            class=\"ButtonRSWP rswp__toggle _ControlsButtonRSWP __3hmsj\"\n            title=\"Play\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-7.61 18.188c-.435.251-.702.715-.701 1.216v25.194a1.402 1.402 0 0 0 2.104 1.214L47.61 33.214a1.402 1.402 0 0 0 0-2.428L25.793 18.188c-.435-.25-.97-.25-1.404 0Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Next\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Next\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M53.486 0a3.2 3.2 0 0 0-3.2 3.2v23.543L4.8.489A3.2 3.2 0 0 0 0 3.255V60.74a3.2 3.2 0 0 0 4.8 2.774l45.486-26.262V60.8a3.2 3.2 0 0 0 3.2 3.2H60.8a3.2 3.2 0 0 0 3.2-3.2V3.2A3.2 3.2 0 0 0 60.8 0h-7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div />\n        <div\n          class=\"rswp__volume\"\n        >\n          <div\n            class=\" _VolumeInlineRSWP __a3qe0k\"\n            data-component-name=\"Volume\"\n            data-value=\"1\"\n          >\n            <span>\n              <svg\n                data-component-name=\"VolumeHigh\"\n                height=\"1em\"\n                preserveAspectRatio=\"xMidYMid\"\n                viewBox=\"0 0 64 64\"\n                width=\"1em\"\n              >\n                <path\n                  d=\"M37.963 3.402a2.989 2.989 0 0 1 1.5 2.596v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0ZM45 9.542a23.008 23.008 0 0 1 0 44.912V48.25a17.008 17.008 0 0 0 0-32.508Zm-11.532 1.656-23.2 13.4a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6ZM45 22.238a11 11 0 0 1 0 19.52v-19.52Z\"\n                  fill=\"currentColor\"\n                />\n              </svg>\n            </span>\n            <div>\n              <div\n                class=\"volume\"\n                data-component-name=\"volume-bar\"\n                style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n              >\n                <div\n                  class=\"volume__track\"\n                  role=\"presentation\"\n                  style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n                >\n                  <div\n                    class=\"volume__range\"\n                    style=\"width: 100%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n                  />\n                  <div\n                    role=\"presentation\"\n                    style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 100%; bottom: 0%;\"\n                  >\n                    <span\n                      aria-label=\"slider handle\"\n                      aria-orientation=\"horizontal\"\n                      aria-valuemax=\"100\"\n                      aria-valuemin=\"0\"\n                      aria-valuenow=\"100\"\n                      class=\"volume__thumb\"\n                      role=\"slider\"\n                      style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                      tabindex=\"0\"\n                    />\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div\n        class=\" _SliderRSWP __q14q70\"\n        data-component-name=\"Slider\"\n        data-position=\"0\"\n      >\n        <div\n          class=\"rswp_progress\"\n        >\n          0:00\n        </div>\n        <div\n          class=\"slider\"\n          data-component-name=\"progress-bar\"\n          style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n        >\n          <div\n            class=\"slider__track\"\n            role=\"presentation\"\n            style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n          >\n            <div\n              class=\"slider__range\"\n              style=\"width: 0%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n            />\n            <div\n              role=\"presentation\"\n              style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 0%; bottom: 0%;\"\n            >\n              <span\n                aria-label=\"slider handle\"\n                aria-orientation=\"horizontal\"\n                aria-valuemax=\"100\"\n                aria-valuemin=\"0\"\n                aria-valuenow=\"0\"\n                class=\"slider__thumb\"\n                role=\"slider\"\n                style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                tabindex=\"0\"\n              />\n            </div>\n          </div>\n        </div>\n        <div\n          class=\"rswp_duration\"\n        >\n          2:31\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`SpotifyWebPlayer > With \"compact\" layout > should render properly 1`] = `\n<div\n  class=\"PlayerRSWP\"\n  data-component-name=\"Player\"\n  data-ready=\"true\"\n  style=\"background: rgb(255, 0, 68); min-height: 80px;\"\n>\n  <div\n    class=\" _WrapperRSWP __5fskss\"\n    data-component-name=\"Wrapper\"\n  >\n    <div\n      class=\"rswp__active _InfoRSWP __7dq9rw\"\n      data-component-name=\"Info\"\n    >\n      <a\n        aria-label=\"Main Theme From Trouble Man on SPOTIFY\"\n        href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n        rel=\"noreferrer\"\n        target=\"_blank\"\n        title=\"Main Theme From Trouble Man on SPOTIFY\"\n      >\n        <img\n          alt=\"Main Theme From Trouble Man\"\n          src=\"https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052\"\n        />\n      </a>\n      <div\n        class=\" _ContentWrapperRSWP __1gmrvbw\"\n      >\n        <div\n          class=\" _ContentRSWP __1xwov57\"\n        >\n          <div\n            data-type=\"title-artist-wrapper\"\n          >\n            <div>\n              <p>\n                <span>\n                  <a\n                    aria-label=\"Main Theme From Trouble Man on SPOTIFY\"\n                    href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    title=\"Main Theme From Trouble Man on SPOTIFY\"\n                  >\n                    Main Theme From Trouble Man\n                  </a>\n                </span>\n              </p>\n              <p\n                title=\"Marvin Gaye\"\n              >\n                <span>\n                  <a\n                    aria-label=\"Marvin Gaye on SPOTIFY\"\n                    href=\"https://open.spotify.com/artist/3koiLjNrgRTNbOwViDipeA\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    title=\"Marvin Gaye on SPOTIFY\"\n                  >\n                    Marvin Gaye\n                  </a>\n                </span>\n              </p>\n            </div>\n          </div>\n        </div>\n        <a\n          aria-label=\"Play on Spotify\"\n          href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n          rel=\"noreferrer\"\n          target=\"_blank\"\n        >\n          <svg\n            height=\"1em\"\n            preserveAspectRatio=\"xMidYMid\"\n            viewBox=\"0 0 512 160\"\n            width=\"3.2em\"\n          >\n            <path\n              d=\"M79.655 0C35.664 0 0 35.663 0 79.654c0 43.993 35.664 79.653 79.655 79.653 43.996 0 79.656-35.66 79.656-79.653 0-43.988-35.66-79.65-79.657-79.65L79.655 0Zm36.53 114.884a4.963 4.963 0 0 1-6.83 1.646c-18.702-11.424-42.246-14.011-69.973-7.676a4.967 4.967 0 0 1-5.944-3.738 4.958 4.958 0 0 1 3.734-5.945c30.343-6.933 56.37-3.948 77.367 8.884a4.965 4.965 0 0 1 1.645 6.83Zm9.75-21.689c-1.799 2.922-5.622 3.845-8.543 2.047-21.41-13.16-54.049-16.972-79.374-9.284a6.219 6.219 0 0 1-7.75-4.138 6.22 6.22 0 0 1 4.141-7.745c28.929-8.778 64.892-4.526 89.48 10.583 2.92 1.798 3.843 5.622 2.045 8.538Zm.836-22.585C101.1 55.362 58.742 53.96 34.231 61.4c-3.936 1.194-8.098-1.028-9.29-4.964a7.453 7.453 0 0 1 4.965-9.294c28.137-8.542 74.912-6.892 104.469 10.655a7.441 7.441 0 0 1 2.606 10.209c-2.092 3.54-6.677 4.707-10.206 2.605h-.004Zm89.944 2.922c-13.754-3.28-16.198-5.581-16.198-10.418 0-4.57 4.299-7.645 10.7-7.645 6.202 0 12.347 2.336 18.796 7.143.19.145.437.203.675.165a.888.888 0 0 0 .6-.367l6.715-9.466a.903.903 0 0 0-.171-1.225c-7.676-6.157-16.313-9.15-26.415-9.15-14.848 0-25.225 8.911-25.225 21.662 0 13.673 8.95 18.515 24.417 22.252 13.155 3.031 15.38 5.57 15.38 10.11 0 5.032-4.49 8.161-11.718 8.161-8.028 0-14.582-2.71-21.906-9.046a.932.932 0 0 0-.656-.218.89.89 0 0 0-.619.313l-7.533 8.96a.906.906 0 0 0 .086 1.256c8.522 7.61 19.004 11.624 30.323 11.624 16 0 26.339-8.742 26.339-22.277.028-11.421-6.81-17.746-23.561-21.821l-.029-.013Zm59.792-13.564c-6.934 0-12.622 2.732-17.321 8.33v-6.3c0-.498-.4-.903-.894-.903h-12.318a.899.899 0 0 0-.894.902v70.009c0 .494.4.903.894.903h12.318a.901.901 0 0 0 .894-.903v-22.097c4.699 5.26 10.387 7.838 17.32 7.838 12.89 0 25.94-9.92 25.94-28.886.019-18.97-13.032-28.894-25.93-28.894l-.01.001Zm11.614 28.893c0 9.653-5.945 16.397-14.468 16.397-8.418 0-14.772-7.048-14.772-16.397 0-9.35 6.354-16.397 14.772-16.397 8.38 0 14.468 6.893 14.468 16.396Zm47.759-28.893c-16.598 0-29.601 12.78-29.601 29.1 0 16.143 12.917 28.784 29.401 28.784 16.655 0 29.696-12.736 29.696-28.991 0-16.2-12.955-28.89-29.496-28.89v-.003Zm0 45.385c-8.827 0-15.485-7.096-15.485-16.497 0-9.444 6.43-16.298 15.285-16.298 8.884 0 15.58 7.093 15.58 16.504 0 9.443-6.468 16.291-15.38 16.291Zm64.937-44.258h-13.554V47.24c0-.497-.4-.902-.894-.902H374.05a.906.906 0 0 0-.904.902v13.855h-5.916a.899.899 0 0 0-.894.902v10.584a.9.9 0 0 0 .894.903h5.916v27.39c0 11.062 5.508 16.674 16.38 16.674 4.413 0 8.075-.914 11.528-2.873a.88.88 0 0 0 .457-.78v-10.083a.896.896 0 0 0-.428-.76.873.873 0 0 0-.876-.039c-2.368 1.19-4.66 1.741-7.229 1.741-3.947 0-5.716-1.798-5.716-5.812V73.49h13.554a.899.899 0 0 0 .894-.903V62.003a.873.873 0 0 0-.884-.903l-.01-.005Zm47.217.054v-1.702c0-5.006 1.921-7.238 6.22-7.238 2.57 0 4.633.51 6.945 1.28a.895.895 0 0 0 1.18-.858l-.001-10.377a.891.891 0 0 0-.637-.865c-2.435-.726-5.555-1.47-10.235-1.47-11.367 0-17.388 6.405-17.388 18.516v2.606h-5.916a.906.906 0 0 0-.904.902v10.638c0 .497.41.903.904.903h5.916v42.237c0 .504.41.904.904.904h12.308c.504 0 .904-.4.904-.904V73.487h11.5l17.616 42.234c-1.998 4.433-3.967 5.317-6.65 5.317-2.168 0-4.46-.646-6.79-1.93a.98.98 0 0 0-.714-.067.896.896 0 0 0-.533.485l-4.175 9.16a.9.9 0 0 0 .39 1.17c4.356 2.359 8.284 3.367 13.145 3.367 9.093 0 14.125-4.242 18.548-15.637l21.364-55.204a.88.88 0 0 0-.095-.838.878.878 0 0 0-.733-.392h-12.822a.901.901 0 0 0-.856.605l-13.136 37.509-14.382-37.534a.898.898 0 0 0-.837-.58h-21.04v-.003Zm-27.375-.054h-12.318a.907.907 0 0 0-.903.902v53.724c0 .504.409.904.903.904h12.318c.495 0 .904-.4.904-.904v-53.72a.9.9 0 0 0-.904-.903v-.003Zm-6.088-24.464c-4.88 0-8.836 3.95-8.836 8.828a8.835 8.835 0 0 0 8.836 8.836c4.88 0 8.827-3.954 8.827-8.836a8.83 8.83 0 0 0-8.827-8.828Z\"\n              fill=\"#ffffff\"\n            />\n          </svg>\n        </a>\n      </div>\n    </div>\n    <div\n      class=\" _ControlsRSWP __anzta0\"\n      data-component-name=\"Controls\"\n      data-playing=\"false\"\n    >\n      <div\n        class=\" _ControlsButtonsRSWP __bd4m75\"\n      >\n        <div\n          class=\"rswp__devices\"\n        >\n          <div>\n            <div\n              class=\" _DevicesRSWP __ix65sv\"\n              data-component-name=\"Devices\"\n              data-device-id=\"19ks98hfbxc53vh34jd\"\n            >\n              <button\n                aria-label=\"Devices\"\n                class=\"ButtonRSWP\"\n                title=\"Devices\"\n                type=\"button\"\n              >\n                <svg\n                  height=\"1em\"\n                  preserveAspectRatio=\"xMidYMid\"\n                  viewBox=\"0 0 64 64\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M57 4c3.864 0 7 3.136 7 7v42a7 7 0 0 1-7 7H31a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h26ZM16 54v6H8v-6h8Zm41-44H31a1 1 0 0 0-1 1v42a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1ZM44 32a8 8 0 1 1 0 16 8 8 0 0 1 0-16ZM16 4v6H7a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h9v6H7a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h9Zm28 12a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n                    fill=\"currentColor\"\n                  />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n        <div />\n        <div>\n          <button\n            aria-label=\"Previous\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Previous\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M10.514 0a3.2 3.2 0 0 1 3.2 3.2v23.543L59.2.489A3.2 3.2 0 0 1 64 3.255V60.74a3.2 3.2 0 0 1-4.8 2.774L13.714 37.253V60.8a3.2 3.2 0 0 1-3.2 3.2H3.2A3.2 3.2 0 0 1 0 60.8V3.2A3.2 3.2 0 0 1 3.2 0h7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Play\"\n            class=\"ButtonRSWP rswp__toggle _ControlsButtonRSWP __3hmsj\"\n            title=\"Play\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-7.61 18.188c-.435.251-.702.715-.701 1.216v25.194a1.402 1.402 0 0 0 2.104 1.214L47.61 33.214a1.402 1.402 0 0 0 0-2.428L25.793 18.188c-.435-.25-.97-.25-1.404 0Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Next\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Next\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M53.486 0a3.2 3.2 0 0 0-3.2 3.2v23.543L4.8.489A3.2 3.2 0 0 0 0 3.255V60.74a3.2 3.2 0 0 0 4.8 2.774l45.486-26.262V60.8a3.2 3.2 0 0 0 3.2 3.2H60.8a3.2 3.2 0 0 0 3.2-3.2V3.2A3.2 3.2 0 0 0 60.8 0h-7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div />\n        <div\n          class=\"rswp__volume\"\n        >\n          <div>\n            <div\n              class=\" _VolumeRSWP __me2fpj\"\n              data-component-name=\"Volume\"\n              data-value=\"1\"\n            >\n              <button\n                aria-label=\"Volume\"\n                class=\"ButtonRSWP\"\n                title=\"Volume\"\n                type=\"button\"\n              >\n                <svg\n                  data-component-name=\"VolumeHigh\"\n                  height=\"1em\"\n                  preserveAspectRatio=\"xMidYMid\"\n                  viewBox=\"0 0 64 64\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M37.963 3.402a2.989 2.989 0 0 1 1.5 2.596v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0ZM45 9.542a23.008 23.008 0 0 1 0 44.912V48.25a17.008 17.008 0 0 0 0-32.508Zm-11.532 1.656-23.2 13.4a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6ZM45 22.238a11 11 0 0 1 0 19.52v-19.52Z\"\n                    fill=\"currentColor\"\n                  />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div\n        class=\" _SliderRSWP __1lztbt5\"\n        data-component-name=\"Slider\"\n        data-position=\"0\"\n      >\n        <div\n          class=\"rswp_progress\"\n        >\n          0:00\n        </div>\n        <div\n          class=\"slider\"\n          data-component-name=\"progress-bar\"\n          style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n        >\n          <div\n            class=\"slider__track\"\n            role=\"presentation\"\n            style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n          >\n            <div\n              class=\"slider__range\"\n              style=\"width: 0%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n            />\n            <div\n              role=\"presentation\"\n              style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 0%; bottom: 0%;\"\n            >\n              <span\n                aria-label=\"slider handle\"\n                aria-orientation=\"horizontal\"\n                aria-valuemax=\"100\"\n                aria-valuemin=\"0\"\n                aria-valuenow=\"0\"\n                class=\"slider__thumb\"\n                role=\"slider\"\n                style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                tabindex=\"0\"\n              />\n            </div>\n          </div>\n        </div>\n        <div\n          class=\"rswp_duration\"\n        >\n          2:31\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`SpotifyWebPlayer > With \"components\" prop > should render the components 1`] = `\n<div\n  class=\" _ControlsRSWP __11aa6ep\"\n  data-component-name=\"Controls\"\n  data-playing=\"false\"\n>\n  <div\n    class=\" _ControlsButtonsRSWP __a3qe0k\"\n  >\n    <div\n      class=\"rswp__devices\"\n    >\n      <div>\n        <div\n          class=\" _DevicesRSWP __10w631a\"\n          data-component-name=\"Devices\"\n          data-device-id=\"19ks98hfbxc53vh34jd\"\n        >\n          <button\n            aria-label=\"Devices\"\n            class=\"ButtonRSWP\"\n            title=\"Devices\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M57 4c3.864 0 7 3.136 7 7v42a7 7 0 0 1-7 7H31a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h26ZM16 54v6H8v-6h8Zm41-44H31a1 1 0 0 0-1 1v42a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1ZM44 32a8 8 0 1 1 0 16 8 8 0 0 1 0-16ZM16 4v6H7a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h9v6H7a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h9Zm28 12a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n      </div>\n    </div>\n    <div>\n      <button\n        type=\"button\"\n      >\n        Left\n      </button>\n    </div>\n    <div>\n      <button\n        aria-label=\"Previous\"\n        class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n        title=\"Previous\"\n        type=\"button\"\n      >\n        <svg\n          height=\"1em\"\n          preserveAspectRatio=\"xMidYMid\"\n          viewBox=\"0 0 64 64\"\n          width=\"1em\"\n        >\n          <path\n            d=\"M10.514 0a3.2 3.2 0 0 1 3.2 3.2v23.543L59.2.489A3.2 3.2 0 0 1 64 3.255V60.74a3.2 3.2 0 0 1-4.8 2.774L13.714 37.253V60.8a3.2 3.2 0 0 1-3.2 3.2H3.2A3.2 3.2 0 0 1 0 60.8V3.2A3.2 3.2 0 0 1 3.2 0h7.314Z\"\n            fill=\"currentColor\"\n          />\n        </svg>\n      </button>\n    </div>\n    <div>\n      <button\n        aria-label=\"Play\"\n        class=\"ButtonRSWP rswp__toggle _ControlsButtonRSWP __3hmsj\"\n        title=\"Play\"\n        type=\"button\"\n      >\n        <svg\n          height=\"1em\"\n          preserveAspectRatio=\"xMidYMid\"\n          viewBox=\"0 0 64 64\"\n          width=\"1em\"\n        >\n          <path\n            d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-7.61 18.188c-.435.251-.702.715-.701 1.216v25.194a1.402 1.402 0 0 0 2.104 1.214L47.61 33.214a1.402 1.402 0 0 0 0-2.428L25.793 18.188c-.435-.25-.97-.25-1.404 0Z\"\n            fill=\"currentColor\"\n          />\n        </svg>\n      </button>\n    </div>\n    <div>\n      <button\n        aria-label=\"Next\"\n        class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n        title=\"Next\"\n        type=\"button\"\n      >\n        <svg\n          height=\"1em\"\n          preserveAspectRatio=\"xMidYMid\"\n          viewBox=\"0 0 64 64\"\n          width=\"1em\"\n        >\n          <path\n            d=\"M53.486 0a3.2 3.2 0 0 0-3.2 3.2v23.543L4.8.489A3.2 3.2 0 0 0 0 3.255V60.74a3.2 3.2 0 0 0 4.8 2.774l45.486-26.262V60.8a3.2 3.2 0 0 0 3.2 3.2H60.8a3.2 3.2 0 0 0 3.2-3.2V3.2A3.2 3.2 0 0 0 60.8 0h-7.314Z\"\n            fill=\"currentColor\"\n          />\n        </svg>\n      </button>\n    </div>\n    <div>\n      <button\n        type=\"button\"\n      >\n        Right\n      </button>\n    </div>\n    <div\n      class=\"rswp__volume\"\n    >\n      <div\n        class=\" _VolumeInlineRSWP __a3qe0k\"\n        data-component-name=\"Volume\"\n        data-value=\"1\"\n      >\n        <span>\n          <svg\n            data-component-name=\"VolumeHigh\"\n            height=\"1em\"\n            preserveAspectRatio=\"xMidYMid\"\n            viewBox=\"0 0 64 64\"\n            width=\"1em\"\n          >\n            <path\n              d=\"M37.963 3.402a2.989 2.989 0 0 1 1.5 2.596v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0ZM45 9.542a23.008 23.008 0 0 1 0 44.912V48.25a17.008 17.008 0 0 0 0-32.508Zm-11.532 1.656-23.2 13.4a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6ZM45 22.238a11 11 0 0 1 0 19.52v-19.52Z\"\n              fill=\"currentColor\"\n            />\n          </svg>\n        </span>\n        <div>\n          <div\n            class=\"volume\"\n            data-component-name=\"volume-bar\"\n            style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n          >\n            <div\n              class=\"volume__track\"\n              role=\"presentation\"\n              style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n            >\n              <div\n                class=\"volume__range\"\n                style=\"width: 100%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n              />\n              <div\n                role=\"presentation\"\n                style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 100%; bottom: 0%;\"\n              >\n                <span\n                  aria-label=\"slider handle\"\n                  aria-orientation=\"horizontal\"\n                  aria-valuemax=\"100\"\n                  aria-valuemin=\"0\"\n                  aria-valuenow=\"100\"\n                  class=\"volume__thumb\"\n                  role=\"slider\"\n                  style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                  tabindex=\"0\"\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n  <div\n    class=\" _SliderRSWP __q14q70\"\n    data-component-name=\"Slider\"\n    data-position=\"0\"\n  >\n    <div\n      class=\"rswp_progress\"\n    >\n      0:00\n    </div>\n    <div\n      class=\"slider\"\n      data-component-name=\"progress-bar\"\n      style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n    >\n      <div\n        class=\"slider__track\"\n        role=\"presentation\"\n        style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n      >\n        <div\n          class=\"slider__range\"\n          style=\"width: 0%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n        />\n        <div\n          role=\"presentation\"\n          style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 0%; bottom: 0%;\"\n        >\n          <span\n            aria-label=\"slider handle\"\n            aria-orientation=\"horizontal\"\n            aria-valuemax=\"100\"\n            aria-valuemin=\"0\"\n            aria-valuenow=\"0\"\n            class=\"slider__thumb\"\n            role=\"slider\"\n            style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n            tabindex=\"0\"\n          />\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"rswp_duration\"\n    >\n      2:31\n    </div>\n  </div>\n</div>\n`;\n\nexports[`SpotifyWebPlayer > With the local player > should render the full UI 1`] = `\n<div\n  class=\"PlayerRSWP\"\n  data-component-name=\"Player\"\n  data-ready=\"true\"\n  style=\"background: rgb(255, 255, 255); min-height: 80px;\"\n>\n  <div\n    class=\" _WrapperRSWP __1g3et1y\"\n    data-component-name=\"Wrapper\"\n  >\n    <div\n      class=\"rswp__active _InfoRSWP __14asrht\"\n      data-component-name=\"Info\"\n    >\n      <a\n        aria-label=\"Main Theme From Trouble Man on SPOTIFY\"\n        href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n        rel=\"noreferrer\"\n        target=\"_blank\"\n        title=\"Main Theme From Trouble Man on SPOTIFY\"\n      >\n        <img\n          alt=\"Main Theme From Trouble Man\"\n          src=\"https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052\"\n        />\n      </a>\n      <div\n        class=\" _ContentWrapperRSWP __1c53jt9\"\n      >\n        <div\n          class=\" _ContentRSWP __mdrp5m\"\n        >\n          <div\n            data-type=\"title-artist-wrapper\"\n          >\n            <div>\n              <p>\n                <span>\n                  <a\n                    aria-label=\"Main Theme From Trouble Man on SPOTIFY\"\n                    href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    title=\"Main Theme From Trouble Man on SPOTIFY\"\n                  >\n                    Main Theme From Trouble Man\n                  </a>\n                </span>\n              </p>\n              <p\n                title=\"Marvin Gaye\"\n              >\n                <span>\n                  <a\n                    aria-label=\"Marvin Gaye on SPOTIFY\"\n                    href=\"https://open.spotify.com/artist/3koiLjNrgRTNbOwViDipeA\"\n                    rel=\"noreferrer\"\n                    target=\"_blank\"\n                    title=\"Marvin Gaye on SPOTIFY\"\n                  >\n                    Marvin Gaye\n                  </a>\n                </span>\n              </p>\n            </div>\n          </div>\n          <button\n            aria-label=\"Save to your favorites\"\n            class=\"ButtonRSWP\"\n            title=\"Save to your favorites\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M5.944 7.206C13.271.3 24.723.34 31.999 7.3A18.924 18.924 0 0 1 48.02 2.32h.008a19.068 19.068 0 0 1 15.617 15.071v.013A18.759 18.759 0 0 1 59.47 33.26L37.573 59.353a7.288 7.288 0 0 1-8.642 1.916 7.276 7.276 0 0 1-2.498-1.912l-21.901-26.1c-6.55-7.671-5.93-19.131 1.408-26.051h.004Zm13.04 1.04a12.726 12.726 0 0 0-9.737 20.997l.021.02 21.905 26.105c.316.372.84.488 1.284.285.143-.066.27-.164.372-.285l21.934-26.137a12.565 12.565 0 0 0 2.808-10.625 12.875 12.875 0 0 0-10.534-10.17 12.714 12.714 0 0 0-10.785 3.37l-.029.029a6.198 6.198 0 0 1-8.444 0l-.037-.033a12.727 12.727 0 0 0-8.758-3.556Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <a\n          aria-label=\"Play on Spotify\"\n          href=\"https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8\"\n          rel=\"noreferrer\"\n          target=\"_blank\"\n        >\n          <svg\n            height=\"1em\"\n            preserveAspectRatio=\"xMidYMid\"\n            viewBox=\"0 0 512 160\"\n            width=\"3.2em\"\n          >\n            <path\n              d=\"M79.655 0C35.664 0 0 35.663 0 79.654c0 43.993 35.664 79.653 79.655 79.653 43.996 0 79.656-35.66 79.656-79.653 0-43.988-35.66-79.65-79.657-79.65L79.655 0Zm36.53 114.884a4.963 4.963 0 0 1-6.83 1.646c-18.702-11.424-42.246-14.011-69.973-7.676a4.967 4.967 0 0 1-5.944-3.738 4.958 4.958 0 0 1 3.734-5.945c30.343-6.933 56.37-3.948 77.367 8.884a4.965 4.965 0 0 1 1.645 6.83Zm9.75-21.689c-1.799 2.922-5.622 3.845-8.543 2.047-21.41-13.16-54.049-16.972-79.374-9.284a6.219 6.219 0 0 1-7.75-4.138 6.22 6.22 0 0 1 4.141-7.745c28.929-8.778 64.892-4.526 89.48 10.583 2.92 1.798 3.843 5.622 2.045 8.538Zm.836-22.585C101.1 55.362 58.742 53.96 34.231 61.4c-3.936 1.194-8.098-1.028-9.29-4.964a7.453 7.453 0 0 1 4.965-9.294c28.137-8.542 74.912-6.892 104.469 10.655a7.441 7.441 0 0 1 2.606 10.209c-2.092 3.54-6.677 4.707-10.206 2.605h-.004Zm89.944 2.922c-13.754-3.28-16.198-5.581-16.198-10.418 0-4.57 4.299-7.645 10.7-7.645 6.202 0 12.347 2.336 18.796 7.143.19.145.437.203.675.165a.888.888 0 0 0 .6-.367l6.715-9.466a.903.903 0 0 0-.171-1.225c-7.676-6.157-16.313-9.15-26.415-9.15-14.848 0-25.225 8.911-25.225 21.662 0 13.673 8.95 18.515 24.417 22.252 13.155 3.031 15.38 5.57 15.38 10.11 0 5.032-4.49 8.161-11.718 8.161-8.028 0-14.582-2.71-21.906-9.046a.932.932 0 0 0-.656-.218.89.89 0 0 0-.619.313l-7.533 8.96a.906.906 0 0 0 .086 1.256c8.522 7.61 19.004 11.624 30.323 11.624 16 0 26.339-8.742 26.339-22.277.028-11.421-6.81-17.746-23.561-21.821l-.029-.013Zm59.792-13.564c-6.934 0-12.622 2.732-17.321 8.33v-6.3c0-.498-.4-.903-.894-.903h-12.318a.899.899 0 0 0-.894.902v70.009c0 .494.4.903.894.903h12.318a.901.901 0 0 0 .894-.903v-22.097c4.699 5.26 10.387 7.838 17.32 7.838 12.89 0 25.94-9.92 25.94-28.886.019-18.97-13.032-28.894-25.93-28.894l-.01.001Zm11.614 28.893c0 9.653-5.945 16.397-14.468 16.397-8.418 0-14.772-7.048-14.772-16.397 0-9.35 6.354-16.397 14.772-16.397 8.38 0 14.468 6.893 14.468 16.396Zm47.759-28.893c-16.598 0-29.601 12.78-29.601 29.1 0 16.143 12.917 28.784 29.401 28.784 16.655 0 29.696-12.736 29.696-28.991 0-16.2-12.955-28.89-29.496-28.89v-.003Zm0 45.385c-8.827 0-15.485-7.096-15.485-16.497 0-9.444 6.43-16.298 15.285-16.298 8.884 0 15.58 7.093 15.58 16.504 0 9.443-6.468 16.291-15.38 16.291Zm64.937-44.258h-13.554V47.24c0-.497-.4-.902-.894-.902H374.05a.906.906 0 0 0-.904.902v13.855h-5.916a.899.899 0 0 0-.894.902v10.584a.9.9 0 0 0 .894.903h5.916v27.39c0 11.062 5.508 16.674 16.38 16.674 4.413 0 8.075-.914 11.528-2.873a.88.88 0 0 0 .457-.78v-10.083a.896.896 0 0 0-.428-.76.873.873 0 0 0-.876-.039c-2.368 1.19-4.66 1.741-7.229 1.741-3.947 0-5.716-1.798-5.716-5.812V73.49h13.554a.899.899 0 0 0 .894-.903V62.003a.873.873 0 0 0-.884-.903l-.01-.005Zm47.217.054v-1.702c0-5.006 1.921-7.238 6.22-7.238 2.57 0 4.633.51 6.945 1.28a.895.895 0 0 0 1.18-.858l-.001-10.377a.891.891 0 0 0-.637-.865c-2.435-.726-5.555-1.47-10.235-1.47-11.367 0-17.388 6.405-17.388 18.516v2.606h-5.916a.906.906 0 0 0-.904.902v10.638c0 .497.41.903.904.903h5.916v42.237c0 .504.41.904.904.904h12.308c.504 0 .904-.4.904-.904V73.487h11.5l17.616 42.234c-1.998 4.433-3.967 5.317-6.65 5.317-2.168 0-4.46-.646-6.79-1.93a.98.98 0 0 0-.714-.067.896.896 0 0 0-.533.485l-4.175 9.16a.9.9 0 0 0 .39 1.17c4.356 2.359 8.284 3.367 13.145 3.367 9.093 0 14.125-4.242 18.548-15.637l21.364-55.204a.88.88 0 0 0-.095-.838.878.878 0 0 0-.733-.392h-12.822a.901.901 0 0 0-.856.605l-13.136 37.509-14.382-37.534a.898.898 0 0 0-.837-.58h-21.04v-.003Zm-27.375-.054h-12.318a.907.907 0 0 0-.903.902v53.724c0 .504.409.904.903.904h12.318c.495 0 .904-.4.904-.904v-53.72a.9.9 0 0 0-.904-.903v-.003Zm-6.088-24.464c-4.88 0-8.836 3.95-8.836 8.828a8.835 8.835 0 0 0 8.836 8.836c4.88 0 8.827-3.954 8.827-8.836a8.83 8.83 0 0 0-8.827-8.828Z\"\n              fill=\"#000000\"\n            />\n          </svg>\n        </a>\n      </div>\n    </div>\n    <div\n      class=\" _ControlsRSWP __11aa6ep\"\n      data-component-name=\"Controls\"\n      data-playing=\"true\"\n    >\n      <div\n        class=\" _ControlsButtonsRSWP __a3qe0k\"\n      >\n        <div\n          class=\"rswp__devices\"\n        >\n          <div>\n            <div\n              class=\" _DevicesRSWP __n5xkwq\"\n              data-component-name=\"Devices\"\n              data-device-id=\"19ks98hfbxc53vh34jd\"\n            >\n              <button\n                aria-label=\"Devices\"\n                class=\"ButtonRSWP\"\n                title=\"Devices\"\n                type=\"button\"\n              >\n                <svg\n                  height=\"1em\"\n                  preserveAspectRatio=\"xMidYMid\"\n                  viewBox=\"0 0 64 64\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M57 4c3.864 0 7 3.136 7 7v42a7 7 0 0 1-7 7H31a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h26ZM16 54v6H8v-6h8Zm41-44H31a1 1 0 0 0-1 1v42a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1ZM44 32a8 8 0 1 1 0 16 8 8 0 0 1 0-16ZM16 4v6H7a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h9v6H7a7 7 0 0 1-7-7V11c0-3.864 3.136-7 7-7h9Zm28 12a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z\"\n                    fill=\"currentColor\"\n                  />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n        <div />\n        <div>\n          <button\n            aria-label=\"Previous\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Previous\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M10.514 0a3.2 3.2 0 0 1 3.2 3.2v23.543L59.2.489A3.2 3.2 0 0 1 64 3.255V60.74a3.2 3.2 0 0 1-4.8 2.774L13.714 37.253V60.8a3.2 3.2 0 0 1-3.2 3.2H3.2A3.2 3.2 0 0 1 0 60.8V3.2A3.2 3.2 0 0 1 3.2 0h7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Pause\"\n            class=\"ButtonRSWP rswp__toggle _ControlsButtonRSWP __3hmsj\"\n            title=\"Pause\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M32 0c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32C14.327 64 0 49.673 0 32 0 14.327 14.327 0 32 0Zm-5.4 18h-5.2a1.4 1.4 0 0 0-1.4 1.4v25.2a1.4 1.4 0 0 0 1.4 1.4h5.2a1.4 1.4 0 0 0 1.4-1.4V19.4a1.4 1.4 0 0 0-1.4-1.4Zm16 0h-5.2a1.4 1.4 0 0 0-1.4 1.4v25.2a1.4 1.4 0 0 0 1.4 1.4h5.2a1.4 1.4 0 0 0 1.4-1.4V19.4a1.4 1.4 0 0 0-1.4-1.4Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div>\n          <button\n            aria-label=\"Next\"\n            class=\"ButtonRSWP _ControlsButtonRSWP __3hmsj\"\n            title=\"Next\"\n            type=\"button\"\n          >\n            <svg\n              height=\"1em\"\n              preserveAspectRatio=\"xMidYMid\"\n              viewBox=\"0 0 64 64\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M53.486 0a3.2 3.2 0 0 0-3.2 3.2v23.543L4.8.489A3.2 3.2 0 0 0 0 3.255V60.74a3.2 3.2 0 0 0 4.8 2.774l45.486-26.262V60.8a3.2 3.2 0 0 0 3.2 3.2H60.8a3.2 3.2 0 0 0 3.2-3.2V3.2A3.2 3.2 0 0 0 60.8 0h-7.314Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </button>\n        </div>\n        <div />\n        <div\n          class=\"rswp__volume\"\n        >\n          <div\n            class=\" _VolumeInlineRSWP __a3qe0k\"\n            data-component-name=\"Volume\"\n            data-value=\"1\"\n          >\n            <span>\n              <svg\n                data-component-name=\"VolumeHigh\"\n                height=\"1em\"\n                preserveAspectRatio=\"xMidYMid\"\n                viewBox=\"0 0 64 64\"\n                width=\"1em\"\n              >\n                <path\n                  d=\"M37.963 3.402a2.989 2.989 0 0 1 1.5 2.596v52a3 3 0 0 1-4.5 2.6l-27.7-16C.32 40.572-2.06 31.688 1.943 24.73a14.556 14.556 0 0 1 5.32-5.328l27.7-16a3 3 0 0 1 3 0ZM45 9.542a23.008 23.008 0 0 1 0 44.912V48.25a17.008 17.008 0 0 0 0-32.508Zm-11.532 1.656-23.2 13.4a8.556 8.556 0 0 0 0 14.8l23.2 13.4v-41.6ZM45 22.238a11 11 0 0 1 0 19.52v-19.52Z\"\n                  fill=\"currentColor\"\n                />\n              </svg>\n            </span>\n            <div>\n              <div\n                class=\"volume\"\n                data-component-name=\"volume-bar\"\n                style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n              >\n                <div\n                  class=\"volume__track\"\n                  role=\"presentation\"\n                  style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n                >\n                  <div\n                    class=\"volume__range\"\n                    style=\"width: 100%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n                  />\n                  <div\n                    role=\"presentation\"\n                    style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 100%; bottom: 0%;\"\n                  >\n                    <span\n                      aria-label=\"slider handle\"\n                      aria-orientation=\"horizontal\"\n                      aria-valuemax=\"100\"\n                      aria-valuemin=\"0\"\n                      aria-valuenow=\"100\"\n                      class=\"volume__thumb\"\n                      role=\"slider\"\n                      style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                      tabindex=\"0\"\n                    />\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div\n        class=\" _SliderRSWP __q14q70\"\n        data-component-name=\"Slider\"\n        data-position=\"0\"\n      >\n        <div\n          class=\"rswp_progress\"\n        >\n          0:00\n        </div>\n        <div\n          class=\"slider\"\n          data-component-name=\"progress-bar\"\n          style=\"box-sizing: border-box; display: inline-block; padding: 0px; transition: height 0.4s, width 0.4s; height: 4px; width: 100%;\"\n        >\n          <div\n            class=\"slider__track\"\n            role=\"presentation\"\n            style=\"background-color: rgb(204, 204, 204); border-radius: 4px; box-sizing: border-box; height: 4px; position: relative; width: 100%;\"\n          >\n            <div\n              class=\"slider__range\"\n              style=\"width: 0%; background-color: rgb(102, 102, 102); border-radius: 4px; position: absolute; height: 100%; top: 0px;\"\n            />\n            <div\n              role=\"presentation\"\n              style=\"box-sizing: border-box; height: 4px; position: absolute; transition: height 0.4s, width 0.4s; width: 20px; left: 0%; bottom: 0%;\"\n            >\n              <span\n                aria-label=\"slider handle\"\n                aria-orientation=\"horizontal\"\n                aria-valuemax=\"100\"\n                aria-valuemin=\"0\"\n                aria-valuenow=\"0\"\n                class=\"slider__thumb\"\n                role=\"slider\"\n                style=\"background-color: rgb(0, 0, 0); border: 0px; border-radius: 50%; box-sizing: border-box; display: block; position: absolute; transition: height 0.4s, width 0.4s; height: 10px; left: -5px; top: -3px; width: 10px;\"\n                tabindex=\"0\"\n              />\n            </div>\n          </div>\n        </div>\n        <div\n          class=\"rswp_duration\"\n        >\n          2:31\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "test/constants.spec.ts",
    "content": "import { ERROR_TYPE, SPOTIFY_CONTENT_TYPE, STATUS, TRANSPARENT_COLOR, TYPE } from '~/constants';\n\ndescribe('ERROR_TYPE', () => {\n  it('should have all options', () => {\n    expect(ERROR_TYPE).toMatchSnapshot();\n  });\n});\n\ndescribe('SPOTIFY_CONTENT_TYPE', () => {\n  it('should have all options', () => {\n    expect(SPOTIFY_CONTENT_TYPE).toMatchSnapshot();\n  });\n});\n\ndescribe('STATUS', () => {\n  it('should have all options', () => {\n    expect(STATUS).toMatchSnapshot();\n  });\n});\n\ndescribe('TRANSPARENT_COLOR', () => {\n  it('should have all options', () => {\n    expect(TRANSPARENT_COLOR).toBe('rgba(0, 0, 0, 0)');\n  });\n});\n\ndescribe('TYPE', () => {\n  it('should have all options', () => {\n    expect(TYPE).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "test/fixtures/data.ts",
    "content": "export const playerAlbum = {\n  images: [\n    {\n      height: 298,\n      url: 'https://i.scdn.co/image/177f29ea8006359bd70784a803a21fea0360ca3e',\n      width: 300,\n    },\n    {\n      height: 64,\n      url: 'https://i.scdn.co/image/38ff482faf9916ca15ccb3e14b2886a27c0866e3',\n      width: 64,\n    },\n    {\n      height: 636,\n      url: 'https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052',\n      width: 640,\n    },\n  ],\n  name: 'Trouble Man',\n  uri: 'spotify:album:7KvKuWUxxNPEU80c4i5AQk',\n};\n\nexport const playerArtists = [\n  {\n    name: 'Marvin Gaye',\n    uri: 'spotify:artist:3koiLjNrgRTNbOwViDipeA',\n  },\n];\n\nexport const playerTrack = {\n  album: playerAlbum,\n  artists: playerArtists,\n  duration_ms: 151626,\n  id: '6KUjwoHktuX3du8laPVfO8',\n  is_playable: true,\n  linked_from: {\n    id: null,\n    uri: null,\n  },\n  linked_from_uri: null,\n  media_type: 'audio',\n  name: 'Main Theme From Trouble Man',\n  type: 'track',\n  uri: 'spotify:track:6KUjwoHktuX3du8laPVfO8',\n};\n\nexport const playerArtistTopTracks = {\n  tracks: [playerTrack],\n};\n\nexport const playerAlbumTracks = {\n  items: [playerTrack],\n};\n\nexport const playerPlaylistTracks = {\n  items: [{ track: playerTrack }],\n};\n\nexport const playerShow = {\n  description: 'A Tasty Treat Podcast for Web Developers',\n  html_description: 'A Tasty Treat Podcast for Web Developers',\n  explicit: false,\n  external_urls: {\n    spotify: 'https://open.spotify.com/show/4kYCRYJ3yK5DQbP5tbfZby',\n  },\n  href: 'https://api.spotify.com/v1/shows/4kYCRYJ3yK5DQbP5tbfZby?locale=en-US%2Cen%3Bq%3D0.9%2Cpt-BR%3Bq%3D0.8%2Cpt%3Bq%3D0.7',\n  id: '4kYCRYJ3yK5DQbP5tbfZby',\n  images: [\n    {\n      height: 640,\n      url: 'https://i.scdn.co/image/ab6765630000ba8ada4bfc6d17ba4b7f66e6012a',\n      width: 640,\n    },\n    {\n      height: 300,\n      url: 'https://i.scdn.co/image/ab67656300005f1fda4bfc6d17ba4b7f66e6012a',\n      width: 300,\n    },\n    {\n      height: 64,\n      url: 'https://i.scdn.co/image/ab6765630000f68dda4bfc6d17ba4b7f66e6012a',\n      width: 64,\n    },\n  ],\n  is_externally_hosted: false,\n  languages: ['en'],\n  media_type: 'mixed',\n  name: 'Syntax - Tasty Web Development Treats',\n  publisher: 'Wes Bos and Scott Tolinski',\n  type: 'show',\n  uri: 'spotify:show:4kYCRYJ3yK5DQbP5tbfZby',\n  total_episodes: 847,\n  episodes: {\n    href: 'https://api.spotify.com/v1/shows/4kYCRYJ3yK5DQbP5tbfZby/episodes?offset=0&limit=50&locale=en-US,en;q%3D0.9,pt-BR;q%3D0.8,pt;q%3D0.7',\n    limit: 50,\n    next: 'https://api.spotify.com/v1/shows/4kYCRYJ3yK5DQbP5tbfZby/episodes?offset=50&limit=50&locale=en-US,en;q%3D0.9,pt-BR;q%3D0.8,pt;q%3D0.7',\n    offset: 0,\n    previous: null,\n    total: 847,\n    items: [\n      {\n        audio_preview_url:\n          'https://podz-content.spotifycdn.com/audio/clips/0qbdMJHfDdYKgaV4fnOF8w/clip_2557400_2604150.mp3',\n        description:\n          ' Scott and Wes unpack their experiences as electric car owners, sharing the highs and lows of making the switch. From range anxiety to charging infrastructure and cost savings, they talk about everything from the tech perks to the unexpected challenges of driving electric. Show Notes   00:00 Welcome to Syntax!  02:11 Brought to you by Sentry.io.  03:14 What cars and how long have we had them.   Hyundai IONIQ 5.  Tesla Model Y Long Range.    10:41 Range and dealing with range anxiety.  11:45 The EPA specs.  12:24 Things that affect range.  14:46 Charging.  17:52  Charging levels.   17:56 Level 1 charging.  19:01 Level 2 charging.  19:39 Level 3 charging.  20:10 Charging standards.    21:51 Electric car pricing.  25:56  Regenerative braking.  27:27 General maintenance.  29:04 Pricing and expenses.  31:48 Machine Gun Kelly Effect.  36:46 Would you go completely electric?  38:46 Electric-only tech.  40:57 Buying a new EV.  42:21 Edison Motors website, TikTok.   Hit us up on Socials!  Syntax: X Instagram Tiktok LinkedIn Threads  Wes: X Instagram Tiktok LinkedIn Threads  Scott: X Instagram Tiktok LinkedIn Threads  Randy: X Instagram YouTube Threads',\n        html_description:\n          '<p> Scott and Wes unpack their experiences as electric car owners, sharing the highs and lows of making the switch. From range anxiety to charging infrastructure and cost savings, they talk about everything from the tech perks to the unexpected challenges of driving electric.</p> <h3>Show Notes</h3> <ul><li> <a href=\"#t&#61;00:00\" rel=\"nofollow\">00:00</a> Welcome to Syntax!</li><li> <a href=\"#t&#61;02:11\" rel=\"nofollow\">02:11</a> Brought to you by <a href=\"https://sentry.io/syntax\" rel=\"nofollow\">Sentry.io</a>.</li><li> <a href=\"#t&#61;03:14\" rel=\"nofollow\">03:14</a> What cars and how long have we had them. <ul><li> <a href=\"https://www.hyundai.com/uk/en/models/new-ioniq5.html\" rel=\"nofollow\">Hyundai IONIQ 5</a>.</li><li> <a href=\"https://ts.la/wes189166\" rel=\"nofollow\">Tesla Model Y Long Range</a>.</li></ul> </li><li> <a href=\"#t&#61;10:41\" rel=\"nofollow\">10:41</a> Range and dealing with range anxiety.</li><li> <a href=\"#t&#61;11:45\" rel=\"nofollow\">11:45</a> The <a href=\"https://www.epa.gov/\" rel=\"nofollow\">EPA</a> specs.</li><li> <a href=\"#t&#61;12:24\" rel=\"nofollow\">12:24</a> Things that affect range.</li><li> <a href=\"#t&#61;14:46\" rel=\"nofollow\">14:46</a> Charging.</li><li> <a href=\"#t&#61;17:52\" rel=\"nofollow\">17:52</a> <a href=\"https://www.transportation.gov/rural/ev/toolkit/ev-basics/charging-speeds\" rel=\"nofollow\"> Charging levels</a>. <ul><li> <a href=\"#t&#61;17:56\" rel=\"nofollow\">17:56</a> Level 1 charging.</li><li> <a href=\"#t&#61;19:01\" rel=\"nofollow\">19:01</a> Level 2 charging.</li><li> <a href=\"#t&#61;19:39\" rel=\"nofollow\">19:39</a> Level 3 charging.</li><li> <a href=\"#t&#61;20:10\" rel=\"nofollow\">20:10</a> <a href=\"https://chargehub.com/en/electric-car-charging-guide.html\" rel=\"nofollow\">Charging standards</a>.</li></ul> </li><li> <a href=\"#t&#61;21:51\" rel=\"nofollow\">21:51</a> Electric car pricing.</li><li> <a href=\"#t&#61;25:56\" rel=\"nofollow\">25:56</a> <a href=\"https://www.sciencedirect.com/science/article/abs/pii/B9780123973146000115\" rel=\"nofollow\"> Regenerative braking</a>.</li><li> <a href=\"#t&#61;27:27\" rel=\"nofollow\">27:27</a> General maintenance.</li><li> <a href=\"#t&#61;29:04\" rel=\"nofollow\">29:04</a> Pricing and expenses.</li><li> <a href=\"#t&#61;31:48\" rel=\"nofollow\">31:48</a> Machine Gun Kelly Effect.</li><li> <a href=\"#t&#61;36:46\" rel=\"nofollow\">36:46</a> Would you go completely electric?</li><li> <a href=\"#t&#61;38:46\" rel=\"nofollow\">38:46</a> Electric-only tech.</li><li> <a href=\"#t&#61;40:57\" rel=\"nofollow\">40:57</a> Buying a new EV.</li><li> <a href=\"#t&#61;42:21\" rel=\"nofollow\">42:21</a> Edison Motors <a href=\"https://www.edisonmotors.ca/\" rel=\"nofollow\">website</a>, <a href=\"https://www.tiktok.com/&#64;_edison.motors?lang&#61;en\" rel=\"nofollow\">TikTok</a>.</li></ul> <h3> Hit us up on Socials!</h3> <p> Syntax: <a href=\"https://twitter.com/syntaxfm\" rel=\"nofollow\">X</a> <a href=\"https://www.instagram.com/syntax_fm/\" rel=\"nofollow\">Instagram</a> <a href=\"https://www.tiktok.com/&#64;syntaxfm\" rel=\"nofollow\">Tiktok</a> <a href=\"https://www.linkedin.com/company/96077407/admin/feed/posts/\" rel=\"nofollow\">LinkedIn</a> <a href=\"https://www.threads.net/&#64;syntax_fm\" rel=\"nofollow\">Threads</a></p> <p> Wes: <a href=\"https://twitter.com/wesbos\" rel=\"nofollow\">X</a> <a href=\"https://www.instagram.com/wesbos/\" rel=\"nofollow\">Instagram</a> <a href=\"https://www.tiktok.com/&#64;wesbos\" rel=\"nofollow\">Tiktok</a> <a href=\"https://www.linkedin.com/in/wesbos/\" rel=\"nofollow\">LinkedIn</a> <a href=\"https://www.threads.net/&#64;wesbos\" rel=\"nofollow\">Threads</a></p> <p> Scott: <a href=\"https://twitter.com/stolinski\" rel=\"nofollow\">X</a> <a href=\"https://www.instagram.com/stolinski/\" rel=\"nofollow\">Instagram</a> <a href=\"https://www.tiktok.com/&#64;stolinski\" rel=\"nofollow\">Tiktok</a> <a href=\"https://www.linkedin.com/in/stolinski/\" rel=\"nofollow\">LinkedIn</a> <a href=\"https://www.threads.net/&#64;stolinski\" rel=\"nofollow\">Threads</a></p> <p> Randy: <a href=\"https://twitter.com/randyrektor\" rel=\"nofollow\">X</a> <a href=\"https://www.instagram.com/randyrektor/\" rel=\"nofollow\">Instagram</a> <a href=\"https://www.youtube.com/&#64;randyrektor\" rel=\"nofollow\">YouTube</a> <a href=\"https://www.threads.net/&#64;randyrektor\" rel=\"nofollow\">Threads</a></p>',\n        duration_ms: 2645609,\n        explicit: false,\n        external_urls: {\n          spotify: 'https://open.spotify.com/episode/5cTJgzcfadfcfPzvJAwJk1',\n        },\n        href: 'https://api.spotify.com/v1/episodes/5cTJgzcfadfcfPzvJAwJk1',\n        id: '5cTJgzcfadfcfPzvJAwJk1',\n        images: [\n          {\n            url: 'https://i.scdn.co/image/ab6765630000ba8ada4bfc6d17ba4b7f66e6012a',\n            height: 640,\n            width: 640,\n          },\n          {\n            url: 'https://i.scdn.co/image/ab67656300005f1fda4bfc6d17ba4b7f66e6012a',\n            height: 300,\n            width: 300,\n          },\n          {\n            url: 'https://i.scdn.co/image/ab6765630000f68dda4bfc6d17ba4b7f66e6012a',\n            height: 64,\n            width: 64,\n          },\n        ],\n        is_externally_hosted: false,\n        is_playable: true,\n        language: 'en',\n        languages: ['en'],\n        name: '846: Talking EVs: Range Anxiety, Charging, and Tech',\n        release_date: '2024-11-11',\n        release_date_precision: 'day',\n        resume_point: {\n          fully_played: false,\n          resume_position_ms: 0,\n        },\n        type: 'episode',\n        uri: 'spotify:episode:5cTJgzcfadfcfPzvJAwJk1',\n      },\n    ],\n  },\n};\n\nexport const playerState = {\n  bitrate: 256000,\n  context: {\n    metadata: {\n      context_description: 'Trouble Man',\n    },\n    uri: 'spotify:album:7KvKuWUxxNPEU80c4i5AQk',\n  },\n  disallows: {\n    resuming: true,\n    skipping_prev: true,\n  },\n  duration: 151626,\n  paused: true,\n  position: 0,\n  repeat_mode: 0,\n  restrictions: {\n    disallow_resuming_reasons: ['not_paused'],\n    disallow_skipping_prev_reasons: ['no_prev_track'],\n  },\n  shuffle: false,\n  timestamp: 1556483439737,\n  track_window: {\n    current_track: playerTrack,\n    next_tracks: [\n      {\n        ...playerTrack,\n        name: 'Next Theme From Trouble Man',\n      },\n    ],\n    previous_tracks: [\n      {\n        ...playerTrack,\n        name: 'Previous Theme From Trouble Man',\n      },\n    ],\n  },\n};\n\nexport const playbackState = {\n  actions: {\n    disallows: {\n      resuming: true,\n      skipping_prev: true,\n    },\n  },\n  context: {\n    external_urls: {\n      spotify: 'https://open.spotify.com/album/7KvKuWUxxNPEU80c4i5AQk',\n    },\n    href: 'https://api.spotify.com/v1/albums/7KvKuWUxxNPEU80c4i5AQk',\n    type: 'album',\n    uri: 'spotify:album:7KvKuWUxxNPEU80c4i5AQk',\n  },\n  currently_playing_type: 'track',\n  device: {\n    id: '84944e58544c5d9ebfa1b9aa1f1890fb03c42250',\n    is_active: true,\n    is_private_session: false,\n    is_restricted: false,\n    name: 'Spotify Web Player',\n    type: 'Computer',\n    volume_percent: 100,\n  },\n  is_playing: false,\n  item: {\n    album: {\n      album_type: 'album',\n      artists: [\n        {\n          external_urls: {\n            spotify: 'https://open.spotify.com/artist/3koiLjNrgRTNbOwViDipeA',\n          },\n          href: 'https://api.spotify.com/v1/artists/3koiLjNrgRTNbOwViDipeA',\n          id: '3koiLjNrgRTNbOwViDipeA',\n          name: 'Marvin Gaye',\n          type: 'artist',\n          uri: 'spotify:artist:3koiLjNrgRTNbOwViDipeA',\n        },\n      ],\n      available_markets: [],\n      external_urls: {\n        spotify: 'https://open.spotify.com/album/7KvKuWUxxNPEU80c4i5AQk',\n      },\n      href: 'https://api.spotify.com/v1/albums/7KvKuWUxxNPEU80c4i5AQk',\n      id: '7KvKuWUxxNPEU80c4i5AQk',\n      images: [\n        {\n          height: 636,\n          url: 'https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052',\n          width: 640,\n        },\n        {\n          height: 298,\n          url: 'https://i.scdn.co/image/177f29ea8006359bd70784a803a21fea0360ca3e',\n          width: 300,\n        },\n        {\n          height: 64,\n          url: 'https://i.scdn.co/image/38ff482faf9916ca15ccb3e14b2886a27c0866e3',\n          width: 64,\n        },\n      ],\n      name: 'Trouble Man',\n      release_date: '1972-12-08',\n      release_date_precision: 'day',\n      total_tracks: 13,\n      type: 'album',\n      uri: 'spotify:album:7KvKuWUxxNPEU80c4i5AQk',\n    },\n    artists: [\n      {\n        external_urls: {\n          spotify: 'https://open.spotify.com/artist/3koiLjNrgRTNbOwViDipeA',\n        },\n        href: 'https://api.spotify.com/v1/artists/3koiLjNrgRTNbOwViDipeA',\n        id: '3koiLjNrgRTNbOwViDipeA',\n        name: 'Marvin Gaye',\n        type: 'artist',\n        uri: 'spotify:artist:3koiLjNrgRTNbOwViDipeA',\n      },\n    ],\n    available_markets: [],\n    disc_number: 1,\n    duration_ms: 151626,\n    explicit: false,\n    external_ids: {\n      isrc: 'USMO17200009',\n    },\n    external_urls: {\n      spotify: 'https://open.spotify.com/track/6KUjwoHktuX3du8laPVfO8',\n    },\n    href: 'https://api.spotify.com/v1/tracks/6KUjwoHktuX3du8laPVfO8',\n    id: '6KUjwoHktuX3du8laPVfO8',\n    is_local: false,\n    name: 'Main Theme From Trouble Man - 2',\n    popularity: 27,\n    preview_url:\n      'https://p.scdn.co/mp3-preview/ec9f4fcea45b0665dd162b69004271fe55174566?cid=adaaf209fb064dfab873a71817029e0d',\n    track_number: 1,\n    type: 'track',\n    uri: 'spotify:track:6KUjwoHktuX3du8laPVfO8',\n  },\n  progress_ms: 10443,\n  repeat_state: 'off',\n  shuffle_state: false,\n  timestamp: 1557288761568,\n};\n\nexport const player = {};\n\nexport const queue = {\n  currently_playing: {\n    album: {\n      album_type: 'compilation',\n      total_tracks: 9,\n      available_markets: ['CA', 'BR', 'IT'],\n      external_urls: {\n        spotify: 'string',\n      },\n      href: 'string',\n      id: '2up3OPMp9Tb4dAKM2erWXQ',\n      images: [\n        {\n          url: 'https://i.scdn.co/image/ab67616d00001e02ff9ca10b55ce82ae553c8228',\n          height: 300,\n          width: 300,\n        },\n      ],\n      name: 'string',\n      release_date: '1981-12',\n      release_date_precision: 'year',\n      restrictions: {\n        reason: 'market',\n      },\n      type: 'album',\n      uri: 'spotify:album:2up3OPMp9Tb4dAKM2erWXQ',\n      copyrights: [\n        {\n          text: 'string',\n          type: 'string',\n        },\n      ],\n      external_ids: {\n        isrc: 'string',\n        ean: 'string',\n        upc: 'string',\n      },\n      genres: ['Egg punk', 'Noise rock'],\n      label: 'string',\n      popularity: 0,\n      album_group: 'compilation',\n      artists: [\n        {\n          external_urls: {\n            spotify: 'string',\n          },\n          href: 'string',\n          id: 'string',\n          name: 'string',\n          type: 'artist',\n          uri: 'string',\n        },\n      ],\n    },\n    artists: [\n      {\n        external_urls: {\n          spotify: 'string',\n        },\n        followers: {\n          href: 'string',\n          total: 0,\n        },\n        genres: ['Prog rock', 'Grunge'],\n        href: 'string',\n        id: 'string',\n        images: [\n          {\n            url: 'https://i.scdn.co/image/ab67616d00001e02ff9ca10b55ce82ae553c8228',\n            height: 300,\n            width: 300,\n          },\n        ],\n        name: 'string',\n        popularity: 0,\n        type: 'artist',\n        uri: 'string',\n      },\n    ],\n    available_markets: ['string'],\n    disc_number: 0,\n    duration_ms: 0,\n    explicit: false,\n    external_ids: {\n      isrc: 'string',\n      ean: 'string',\n      upc: 'string',\n    },\n    external_urls: {\n      spotify: 'string',\n    },\n    href: 'string',\n    id: 'string',\n    is_playable: false,\n    linked_from: {},\n    restrictions: {\n      reason: 'string',\n    },\n    name: 'string',\n    popularity: 0,\n    preview_url: 'string',\n    track_number: 0,\n    type: 'track',\n    uri: 'string',\n    is_local: false,\n  },\n  queue: [\n    {\n      album: {\n        album_type: 'compilation',\n        total_tracks: 9,\n        available_markets: ['CA', 'BR', 'IT'],\n        external_urls: {\n          spotify: 'string',\n        },\n        href: 'string',\n        id: '2up3OPMp9Tb4dAKM2erWXQ',\n        images: [\n          {\n            url: 'https://i.scdn.co/image/ab67616d00001e02ff9ca10b55ce82ae553c8228',\n            height: 300,\n            width: 300,\n          },\n        ],\n        name: 'string',\n        release_date: '1981-12',\n        release_date_precision: 'year',\n        restrictions: {\n          reason: 'market',\n        },\n        type: 'album',\n        uri: 'spotify:album:2up3OPMp9Tb4dAKM2erWXQ',\n        copyrights: [\n          {\n            text: 'string',\n            type: 'string',\n          },\n        ],\n        external_ids: {\n          isrc: 'string',\n          ean: 'string',\n          upc: 'string',\n        },\n        genres: ['Egg punk', 'Noise rock'],\n        label: 'string',\n        popularity: 0,\n        album_group: 'compilation',\n        artists: [\n          {\n            external_urls: {\n              spotify: 'string',\n            },\n            href: 'string',\n            id: 'string',\n            name: 'string',\n            type: 'artist',\n            uri: 'string',\n          },\n        ],\n      },\n      artists: [\n        {\n          external_urls: {\n            spotify: 'string',\n          },\n          followers: {\n            href: 'string',\n            total: 0,\n          },\n          genres: ['Prog rock', 'Grunge'],\n          href: 'string',\n          id: 'string',\n          images: [\n            {\n              url: 'https://i.scdn.co/image/ab67616d00001e02ff9ca10b55ce82ae553c8228',\n              height: 300,\n              width: 300,\n            },\n          ],\n          name: 'string',\n          popularity: 0,\n          type: 'artist',\n          uri: 'string',\n        },\n      ],\n      available_markets: ['string'],\n      disc_number: 0,\n      duration_ms: 0,\n      explicit: false,\n      external_ids: {\n        isrc: 'string',\n        ean: 'string',\n        upc: 'string',\n      },\n      external_urls: {\n        spotify: 'string',\n      },\n      href: 'string',\n      id: 'string',\n      is_playable: false,\n      linked_from: {},\n      restrictions: {\n        reason: 'string',\n      },\n      name: 'string',\n      popularity: 0,\n      preview_url: 'string',\n      track_number: 0,\n      type: 'track',\n      uri: 'string',\n      is_local: false,\n    },\n  ],\n};\n"
  },
  {
    "path": "test/fixtures/helpers.ts",
    "content": "export const domRect = {\n  slider: {\n    bottom: 50,\n    height: 6,\n    left: 0,\n    right: 0,\n    top: 0,\n    width: 1024,\n  },\n  volume: {\n    bottom: 50,\n    height: 50,\n    left: 900,\n    right: 0,\n    top: 0,\n    width: 6,\n  },\n  volumeInline: {\n    bottom: 930,\n    height: 4,\n    left: 900,\n    right: 1000,\n    top: 926,\n    width: 100,\n  },\n};\n\nexport function setBoundingClientRect(type: 'slider' | 'volume' | 'volumeInline') {\n  // @ts-ignore\n  Element.prototype.getBoundingClientRect = () => domRect[type];\n}\n"
  },
  {
    "path": "test/index.spec.tsx",
    "content": "/* eslint-disable testing-library/no-unnecessary-act */\nimport React from 'react';\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitFor as testingLibraryWaitFor,\n  within,\n} from '@testing-library/react';\n\nimport SpotifyWebPlayer, { Props } from '~/index';\nimport * as helpers from '~/modules/helpers';\n\nimport { playbackState, playerAlbumTracks, playerState, playerTrack } from './fixtures/data';\nimport { setBoundingClientRect } from './fixtures/helpers';\n\nvi.spyOn(helpers, 'loadSpotifyPlayer').mockImplementation(() => Promise.resolve());\n\nvi.useFakeTimers();\n\nlet playerStateResponse = playerState;\nlet playerStatusResponse = playbackState;\n\nasync function waitFor(fn: () => void) {\n  vi.useRealTimers();\n\n  await testingLibraryWaitFor(fn);\n\n  vi.useFakeTimers();\n}\n\nconst mockAddListener = vi.fn();\n\nconst initializePlayer = async () => {\n  const [, readyFn] = mockAddListener.mock.calls.find(d => d[0] === 'ready')!;\n\n  await readyFn({ device_id: deviceId });\n};\n\nconst updatePlayer = async (state?: Partial<Spotify.PlaybackState>) => {\n  const [, stateChangeFn] = mockAddListener.mock.calls.find(d => d[0] === 'player_state_changed')!;\n\n  await stateChangeFn({ ...playerState, ...state });\n};\n\nconst mockFn = vi.fn();\nconst mockActivateElement = vi.fn();\nconst mockCallback = vi.fn();\nconst mockConnect = vi.fn();\nconst mockDisconnect = vi.fn();\nconst mockGetCurrentState = vi.fn(() => playerStateResponse);\nconst mockGetOAuthToken = vi.fn();\nconst mockGetVolume = vi.fn(() => 1);\nconst mockNextTrack = vi.fn(updatePlayer);\nconst mockPreviousTrack = vi.fn(updatePlayer);\nconst mockRemoveListener = vi.fn();\nconst mockSetName = vi.fn();\nconst mockSetVolume = vi.fn();\nconst mockTogglePlay = vi.fn(updatePlayer);\n\nconst deviceId = '19ks98hfbxc53vh34jd';\nconst externalDeviceId = 'df17372ghs982js892js';\nconst token =\n  'BQDoGCFtLXDAVgphhrRSPFHmhG9ZND3BLzSE5WVE-2qoe7_YZzRcVtZ6F7qEhzTih45GyxZLhp9b53A1YAPObAgV0MDvsbcQg-gZzlrIeQwwsWnz3uulVvPMhqssNP5HnE5SX0P0wTOOta1vneq2dL4Hvdko5WqvRivrEKWXCvJTPAFStfa5V5iLdCSglg';\nconst trackUris = ['spotify:track:2ViHeieFA3iPmsBya2NDFl', 'spotify:track:5zq709Rk69kjzCDdNthSbK'];\n\nconst baseProps = {\n  callback: mockCallback,\n  token,\n  uris: 'spotify:album:7KvKuWUxxNPEU80c4i5AQk',\n};\n\ninterface SetupProps extends Partial<Props> {\n  initialize?: boolean;\n  skipUpdate?: boolean;\n  updateState?: Partial<Spotify.PlaybackState>;\n}\n\nclass Player {\n  _options: any;\n\n  constructor(options: Record<string, any>) {\n    options.getOAuthToken(mockGetOAuthToken);\n\n    // eslint-disable-next-line no-underscore-dangle\n    this._options = options;\n  }\n\n  activateElement = mockActivateElement;\n  addListener = mockAddListener;\n  connect = mockConnect;\n  disconnect = mockDisconnect;\n  getCurrentState = mockGetCurrentState;\n  getVolume = mockGetVolume;\n  nextTrack = mockNextTrack;\n  on = mockFn;\n  pause = mockFn;\n  previousTrack = mockPreviousTrack;\n  removeListener = mockRemoveListener;\n  resume = mockFn;\n  seek = mockFn;\n  setName = mockSetName;\n  setVolume = mockSetVolume;\n  togglePlay = mockTogglePlay;\n}\n\nfunction setExternalDevice() {\n  // open the device selector\n  fireEvent.click(screen.getByLabelText('Devices'));\n\n  // select the external device\n  fireEvent.click(screen.getByLabelText('Test Player'));\n}\n\nasync function setup(props?: SetupProps) {\n  const { initialize = true, skipUpdate = false, updateState, ...rest } = props || {};\n  const view = render(<SpotifyWebPlayer {...baseProps} {...rest} />);\n\n  await act(async () => {\n    window.onSpotifyWebPlaybackSDKReady();\n  });\n\n  if (initialize) {\n    await act(async () => {\n      await initializePlayer();\n\n      if (!skipUpdate) {\n        await updatePlayer(updateState);\n      }\n    });\n  }\n\n  return view;\n}\n\ndescribe('SpotifyWebPlayer', () => {\n  beforeAll(async () => {\n    window.Spotify = {\n      // @ts-expect-error Mock\n      Player,\n    };\n\n    fetchMock.mockIf(/.*/, request => {\n      const { method, url } = request;\n\n      if (url.match(/contains\\?ids=*/)) {\n        return Promise.resolve({\n          body: JSON.stringify([false]),\n        });\n      } else if (url.match(/album/)) {\n        return Promise.resolve({\n          body: JSON.stringify(playerAlbumTracks),\n        });\n      } else if (url.match(/track/)) {\n        return Promise.resolve({\n          body: JSON.stringify(playerTrack),\n        });\n      } else if (url === 'https://api.spotify.com/v1/me/player/devices') {\n        return Promise.resolve({\n          body: JSON.stringify({\n            devices: [\n              {\n                id: externalDeviceId,\n                name: 'Test Player',\n                type: 'Computer',\n              },\n            ],\n          }),\n        });\n      } else if (url === 'https://api.spotify.com/v1/me/player') {\n        return Promise.resolve({\n          body: JSON.stringify(playerStatusResponse),\n        });\n      } else if (method === 'GET' && url === 'https://api.spotify.com/v1/me/tracks') {\n        return Promise.resolve({ status: 200 });\n      } else if (method === 'PUT' && url === 'https://api.spotify.com/v1/me/tracks') {\n        return Promise.resolve({ status: 200 });\n      } else if (['POST', 'PUT'].includes(request.method)) {\n        return Promise.resolve({ status: 204 });\n      }\n\n      return Promise.resolve({ status: 404 });\n    });\n  });\n\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  describe('Error listeners', () => {\n    beforeAll(() => {\n      vi.clearAllMocks();\n    });\n\n    it('should handle `authentication_error`', async () => {\n      const { rerender, unmount } = await setup({ initialize: false });\n\n      const [authenticationType, authenticationFn] = mockAddListener.mock.calls.find(\n        d => d[0] === 'authentication_error',\n      )!;\n\n      await act(async () => {\n        authenticationFn({ type: authenticationType, message: 'Failed to authenticate' });\n      });\n\n      expect(screen.getByTestId('Player')).toHaveAttribute('data-ready', 'false');\n      expect(screen.getByTestId('Player')).toMatchSnapshot('With Error');\n      expect(mockDisconnect).toHaveBeenCalledTimes(1);\n\n      rerender(<SpotifyWebPlayer {...baseProps} token={`${token}BB`} />);\n\n      await act(async () => {\n        await initializePlayer();\n      });\n\n      expect(screen.getByTestId('Player')).toHaveAttribute('data-ready', 'true');\n\n      unmount();\n    });\n\n    it('should handle `account_error`', async () => {\n      await setup({ initialize: false });\n\n      const [accountType, accountFn] = mockAddListener.mock.calls.find(\n        d => d[0] === 'account_error',\n      )!;\n\n      await act(async () => {\n        accountFn({ type: accountType, message: 'Failed to validate Spotify account' });\n      });\n\n      expect(screen.getByTestId('Player')).toMatchSnapshot();\n      expect(mockDisconnect).toHaveBeenCalledTimes(1);\n    });\n\n    it('should handle `initialization_error`', async () => {\n      await setup({ initialize: false });\n\n      const [initializationType, initializationFn] = mockAddListener.mock.calls.find(\n        d => d[0] === 'initialization_error',\n      )!;\n\n      await act(async () => {\n        initializationFn({ type: initializationType, message: 'Failed to initialize' });\n      });\n\n      expect(screen.getByTestId('Player')).toMatchSnapshot();\n      expect(mockDisconnect).toHaveBeenCalledTimes(1);\n    });\n\n    it('should handle `playback_error`', async () => {\n      const { unmount } = await setup({ initialize: true });\n\n      const [playbackType, playbackFn] = mockAddListener.mock.calls.find(\n        d => d[0] === 'playback_error',\n      )!;\n\n      await act(async () => {\n        playbackFn({ type: playbackType, message: 'Failed to perform playback' });\n      });\n\n      expect(screen.getByTestId('Player')).toMatchSnapshot();\n      expect(mockDisconnect).not.toHaveBeenCalled();\n\n      unmount();\n\n      expect(mockDisconnect).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('Device listeners', () => {\n    beforeAll(() => {\n      vi.clearAllMocks();\n    });\n\n    it('should handle `ready`', async () => {\n      await setup({ skipUpdate: true });\n\n      expect(screen.getByTestId('Player')).toMatchSnapshot();\n    });\n\n    it('should handle `player_state_changed`', async () => {\n      await setup({\n        updateState: {\n          paused: false,\n        },\n      });\n\n      await waitFor(() => {\n        expect(screen.getByLabelText('Pause')).toBeInTheDocument();\n      });\n\n      expect(screen.getByTestId('Player')).toMatchSnapshot();\n    });\n  });\n\n  describe('With the local player', () => {\n    const props = { autoPlay: true, showSaveIcon: true };\n\n    beforeAll(() => {\n      vi.clearAllMocks();\n    });\n\n    it('should initialize the token', async () => {\n      await setup(props);\n\n      expect(mockGetOAuthToken).toHaveBeenCalledWith(token);\n    });\n\n    it('should render a loader while initializing', async () => {\n      await setup({ initialize: false });\n\n      expect(screen.getByTestId('Loader')).toBeInTheDocument();\n    });\n\n    it('should render the full UI', async () => {\n      await setup(props);\n\n      expect(screen.getByTestId('Player')).toMatchSnapshot();\n    });\n\n    it('should handle range changes', async () => {\n      setBoundingClientRect('slider');\n      await setup(props);\n      const range = screen.getByTestId('Slider');\n\n      // eslint-disable-next-line testing-library/no-node-access\n      fireEvent.click(range.querySelector('.slider__track')!, {\n        clientX: 410,\n        clientY: 718,\n        currentTarget: {},\n      });\n\n      await waitFor(() => {\n        expect(screen.getByTestId('Slider')).toHaveAttribute('data-position', '40');\n      });\n    });\n\n    it('should handle range magnification', async () => {\n      await setup({ ...props, magnifySliderOnHover: true });\n      const progressBar = screen.getByTestId('progress-bar');\n\n      expect(screen.getAllByLabelText('slider handle')[0]).toHaveStyle({\n        height: '10px',\n      });\n\n      // eslint-disable-next-line testing-library/no-node-access\n      fireEvent.mouseEnter(progressBar.querySelector('.slider__track')!);\n\n      expect(within(progressBar).getByLabelText('slider handle')).toHaveStyle({\n        height: '14px',\n      });\n\n      // eslint-disable-next-line testing-library/no-node-access\n      fireEvent.mouseLeave(progressBar.querySelector('.slider__track')!);\n\n      expect(within(progressBar).getByLabelText('slider handle')).toHaveStyle({\n        height: '10px',\n      });\n    });\n\n    it('should handle Volume changes', async () => {\n      setBoundingClientRect('volume');\n\n      await setup({ ...props, inlineVolume: false });\n\n      expect(screen.getByTestId('VolumeHigh')).toBeInTheDocument();\n\n      fireEvent.click(screen.getByLabelText('Volume'));\n\n      // eslint-disable-next-line testing-library/no-node-access\n      fireEvent.click(screen.getByTestId('Volume').querySelector('.volume__track')!, {\n        clientX: 910,\n        clientY: 25,\n        currentTarget: {},\n      });\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(mockSetVolume).toHaveBeenCalledWith(0.5);\n      expect(screen.getByTestId('Volume')).toHaveAttribute('data-value', '0.5');\n      expect(screen.getByTestId('VolumeMid')).toBeInTheDocument();\n\n      fireEvent.click(screen.getByLabelText('Volume'));\n\n      // eslint-disable-next-line testing-library/no-node-access\n      fireEvent.click(screen.getByTestId('Volume').querySelector('.volume__track')!, {\n        clientX: 910,\n        clientY: 35,\n        currentTarget: {},\n      });\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(mockSetVolume).toHaveBeenCalledWith(0.3);\n      expect(screen.getByTestId('Volume')).toHaveAttribute('data-value', '0.3');\n      expect(screen.getByTestId('VolumeLow')).toBeInTheDocument();\n\n      fireEvent.click(screen.getByLabelText('Volume'));\n\n      // eslint-disable-next-line testing-library/no-node-access\n      fireEvent.click(screen.getByTestId('Volume').querySelector('.volume__track')!, {\n        clientX: 910,\n        clientY: 50,\n        currentTarget: {},\n      });\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(mockSetVolume).toHaveBeenCalledWith(0);\n      expect(screen.getByTestId('Volume')).toHaveAttribute('data-value', '0');\n      expect(screen.getByTestId('VolumeMute')).toBeInTheDocument();\n    });\n\n    it('should handle Volume changes with \"inlineVolume\"', async () => {\n      setBoundingClientRect('volumeInline');\n\n      await setup(props);\n\n      expect(screen.getByTestId('VolumeHigh')).toBeInTheDocument();\n\n      // eslint-disable-next-line testing-library/no-node-access\n      const getTrack = () => screen.getByTestId('Volume').querySelector('.volume__track')!;\n\n      fireEvent.click(getTrack(), {\n        clientX: 950,\n        clientY: 52,\n        currentTarget: {},\n      });\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(mockSetVolume).toHaveBeenCalledWith(0.5);\n      expect(screen.getByTestId('Volume')).toHaveAttribute('data-value', '0.5');\n      expect(screen.getByTestId('VolumeMid')).toBeInTheDocument();\n\n      fireEvent.click(getTrack(), {\n        clientX: 930,\n        clientY: 52,\n        currentTarget: {},\n      });\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(mockSetVolume).toHaveBeenCalledWith(0.3);\n      expect(screen.getByTestId('Volume')).toHaveAttribute('data-value', '0.3');\n      expect(screen.getByTestId('VolumeLow')).toBeInTheDocument();\n\n      fireEvent.click(getTrack(), {\n        clientX: 900,\n        clientY: 52,\n        currentTarget: {},\n      });\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(mockSetVolume).toHaveBeenCalledWith(0);\n      expect(screen.getByTestId('Volume')).toHaveAttribute('data-value', '0');\n      expect(screen.getByTestId('VolumeMute')).toBeInTheDocument();\n    });\n\n    it('should handle repeat changes', async () => {\n      await setup({\n        initialize: true,\n        updateState: {\n          repeat_mode: 1,\n        },\n      });\n\n      await waitFor(() => {\n        expect(mockCallback).toHaveBeenLastCalledWith(\n          expect.objectContaining({ repeat: 'context' }),\n        );\n      });\n    });\n\n    it('should handle shuffle changes', async () => {\n      await setup({\n        initialize: true,\n        updateState: {\n          shuffle: true,\n        },\n      });\n\n      await waitFor(() => {\n        expect(mockCallback).toHaveBeenLastCalledWith(expect.objectContaining({ shuffle: true }));\n      });\n    });\n\n    it('should handle URIs changes', async () => {\n      const { rerender } = await setup(props);\n\n      rerender(<SpotifyWebPlayer {...baseProps} offset={1} uris={trackUris} />);\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/player/play?device_id=19ks98hfbxc53vh34jd',\n        expect.any(Object),\n      );\n      expect(mockTogglePlay).toHaveBeenCalledTimes(1);\n\n      await waitFor(() => {\n        expect(screen.getByTestId('Controls')).toHaveAttribute('data-playing', 'false');\n      });\n    });\n\n    it('should handle Control clicks', async () => {\n      await setup(props);\n\n      // Click the previous track\n      fireEvent.click(screen.getByLabelText('Previous'));\n      expect(mockPreviousTrack).toHaveBeenCalled();\n\n      // Play the next track\n      fireEvent.click(screen.getByLabelText('Next'));\n      expect(mockNextTrack).toHaveBeenCalled();\n\n      fireEvent.click(screen.getByLabelText('Pause'));\n\n      await waitFor(() => {\n        expect(screen.getByTestId('Controls')).toHaveAttribute('data-playing', 'false');\n      });\n    });\n\n    it('should handle Info clicks', async () => {\n      await setup(props);\n\n      fireEvent.click(screen.getByLabelText('Save to your favorites'));\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/tracks',\n        expect.objectContaining({ method: 'PUT' }),\n      );\n\n      await waitFor(() => {\n        expect(screen.getByLabelText('Remove from your favorites')).toBeInTheDocument();\n      });\n    });\n  });\n\n  describe('With an external device', () => {\n    const props: SetupProps = { autoPlay: true, showSaveIcon: true };\n\n    it('should handle Device selection', async () => {\n      await setup(props);\n\n      setExternalDevice();\n\n      expect(screen.getByTestId('Devices')).toHaveAttribute('data-device-id', externalDeviceId);\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/player',\n        expect.objectContaining({ method: 'GET' }),\n      );\n    });\n\n    it('should handle Volume changes', async () => {\n      setBoundingClientRect('volumeInline');\n      playerStatusResponse = {\n        ...playbackState,\n        device: {\n          ...playbackState.device,\n          volume_percent: 60,\n        },\n      };\n      await setup(props);\n\n      setExternalDevice();\n\n      // eslint-disable-next-line testing-library/no-node-access\n      fireEvent.click(screen.getByTestId('Volume').querySelector('.volume__track')!, {\n        clientX: 950,\n        clientY: 50,\n        currentTarget: {},\n      });\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(fetchMock).toHaveBeenCalledWith(\n        'https://api.spotify.com/v1/me/player/volume?volume_percent=50',\n        expect.objectContaining({ method: 'PUT' }),\n      );\n      expect(screen.getByTestId('Volume')).toHaveAttribute('data-value', '0.5');\n    });\n\n    it('should handle Control clicks', async () => {\n      // reset the response (playing)\n      playerStatusResponse = {\n        ...playbackState,\n        is_playing: true,\n      };\n\n      await setup({ ...props, autoPlay: false });\n\n      setExternalDevice();\n\n      fireEvent.click(screen.getByLabelText('Play'));\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        `https://api.spotify.com/v1/me/player/play?device_id=${externalDeviceId}`,\n        expect.any(Object),\n      );\n\n      await waitFor(() => {\n        expect(screen.getByTestId('Controls')).toHaveAttribute('data-playing', 'true');\n      });\n\n      // Play the previous track\n      await act(async () => {\n        fireEvent.click(screen.getByLabelText('Previous'));\n      });\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/player/previous',\n        expect.any(Object),\n      );\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/player',\n        expect.any(Object),\n      );\n\n      // Play the next track\n      await act(async () => {\n        fireEvent.click(screen.getByLabelText('Next'));\n      });\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/player/next',\n        expect.any(Object),\n      );\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/player',\n        expect.any(Object),\n      );\n\n      // Pause the player\n      await act(async () => {\n        fireEvent.click(screen.getByLabelText('Pause'));\n      });\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/player/pause',\n        expect.any(Object),\n      );\n\n      // reset the response again (paused)\n      playerStatusResponse = playbackState;\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        'https://api.spotify.com/v1/me/player',\n        expect.any(Object),\n      );\n\n      await waitFor(() => {\n        expect(screen.getByTestId('Controls')).toHaveAttribute('data-playing', 'false');\n      });\n    });\n  });\n\n  describe('With \"persistDeviceSelection\"', () => {\n    it('should handle \"persistDeviceSelection\"', async () => {\n      await setup({ persistDeviceSelection: true });\n\n      expect(sessionStorage.getItem('rswpDeviceId')).toBe(deviceId);\n\n      setExternalDevice();\n\n      await waitFor(() => {\n        expect(sessionStorage.getItem('rswpDeviceId')).toBe(externalDeviceId);\n      });\n    });\n  });\n\n  describe('With \"syncExternalDevice\"', () => {\n    it('should handle syncExternalDevice changes', async () => {\n      playerStatusResponse = {\n        ...playbackState,\n        is_playing: true,\n      };\n\n      await setup({ syncExternalDevice: true, uris: undefined });\n\n      expect(screen.getByTestId('Devices')).toHaveAttribute(\n        'data-device-id',\n        playbackState.device.id,\n      );\n    });\n  });\n\n  describe('With control props', () => {\n    it('should honor the \"play\" prop', async () => {\n      playerStatusResponse = {\n        ...playbackState,\n        is_playing: true,\n      };\n\n      const { rerender } = await setup({ play: true });\n\n      expect(screen.getByTestId('Controls')).toHaveAttribute('data-playing', 'true');\n\n      playerStateResponse = {\n        ...playerState,\n        paused: true,\n      };\n\n      rerender(<SpotifyWebPlayer {...baseProps} play={false} />);\n\n      await act(async () => {\n        vi.runOnlyPendingTimers();\n      });\n\n      expect(screen.getByTestId('Controls')).toHaveAttribute('data-playing', 'false');\n    });\n\n    it('should handle \"offset\" updates', async () => {\n      const props = {\n        autoPlay: true,\n        uris: trackUris,\n      };\n\n      const { rerender } = await setup(props);\n\n      expect(screen.getByTestId('Controls')).toHaveAttribute('data-playing', 'true');\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        `https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`,\n        expect.objectContaining({\n          body: JSON.stringify({\n            uris: trackUris,\n            offset: { position: 0 },\n          }),\n        }),\n      );\n\n      rerender(<SpotifyWebPlayer {...baseProps} {...props} offset={1} />);\n\n      expect(fetchMock).toHaveBeenLastCalledWith(\n        `https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`,\n        expect.objectContaining({\n          body: JSON.stringify({\n            uris: trackUris,\n            offset: { position: 1 },\n          }),\n        }),\n      );\n    });\n  });\n\n  describe('With \"compact\" layout', () => {\n    it('should render properly', async () => {\n      await setup({ layout: 'compact', styles: { bgColor: '#f04', color: '#fff' } });\n\n      expect(screen.getByTestId('Player')).toMatchSnapshot();\n    });\n  });\n\n  describe('With \"getPlayer\" prop', () => {\n    it('should return the player', async () => {\n      const mockGetPlayer = vi.fn();\n\n      await setup({ getPlayer: mockGetPlayer });\n\n      expect(mockGetPlayer).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('With \"components\" prop', () => {\n    it('should render the components', async () => {\n      await setup({\n        components: {\n          leftButton: <button type=\"button\">Left</button>,\n          rightButton: <button type=\"button\">Right</button>,\n        },\n      });\n\n      expect(screen.getByTestId('Controls')).toMatchSnapshot();\n    });\n  });\n\n  describe('With \"preloadData\" prop', () => {\n    it('should have preloaded the first track of the album', async () => {\n      await setup({ preloadData: true });\n\n      expect(fetchMock).toHaveBeenNthCalledWith(\n        2,\n        'https://api.spotify.com/v1/albums/7KvKuWUxxNPEU80c4i5AQk/tracks',\n        expect.any(Object),\n      );\n\n      expect(fetchMock).toHaveBeenNthCalledWith(\n        3,\n        'https://api.spotify.com/v1/tracks/6KUjwoHktuX3du8laPVfO8',\n        expect.any(Object),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/modules/__snapshots__/getters.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`getLocale > should return a merged locale 1`] = `\n{\n  \"currentDevice\": \"Selected device \",\n  \"devices\": \"Devices\",\n  \"next\": \"Next\",\n  \"otherDevices\": \"Select other device\",\n  \"pause\": \"Pause\",\n  \"play\": \"Play\",\n  \"previous\": \"Previous\",\n  \"removeTrack\": \"Remove from your favorites\",\n  \"saveTrack\": \"Save to your favorites\",\n  \"title\": \"{name} on SPOTIFY\",\n  \"volume\": \"Volume\",\n}\n`;\n\nexports[`getMergedStyles > should return a merged styles 1`] = `\n{\n  \"activeColor\": \"#1cb954\",\n  \"bgColor\": \"rgba(0, 0, 0, 0)\",\n  \"color\": \"#333\",\n  \"errorColor\": \"#ff0026\",\n  \"height\": 100,\n  \"loaderColor\": \"#ccc\",\n  \"loaderSize\": 32,\n  \"sliderColor\": \"#666\",\n  \"sliderHandleBorderRadius\": \"50%\",\n  \"sliderHandleColor\": \"#000\",\n  \"sliderHeight\": 4,\n  \"sliderTrackBorderRadius\": 4,\n  \"sliderTrackColor\": \"#ccc\",\n  \"trackArtistColor\": \"#666\",\n  \"trackNameColor\": \"#333\",\n}\n`;\n\nexports[`getPreloadData > should handle albums 1`] = `\n{\n  \"artists\": [\n    {\n      \"name\": \"Marvin Gaye\",\n      \"uri\": \"spotify:artist:3koiLjNrgRTNbOwViDipeA\",\n    },\n  ],\n  \"durationMs\": 151626,\n  \"id\": \"6KUjwoHktuX3du8laPVfO8\",\n  \"image\": \"https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052\",\n  \"name\": \"Main Theme From Trouble Man\",\n  \"uri\": \"spotify:track:6KUjwoHktuX3du8laPVfO8\",\n}\n`;\n\nexports[`getPreloadData > should handle artist 1`] = `\n{\n  \"artists\": [\n    {\n      \"name\": \"Marvin Gaye\",\n      \"uri\": \"spotify:artist:3koiLjNrgRTNbOwViDipeA\",\n    },\n  ],\n  \"durationMs\": 151626,\n  \"id\": \"6KUjwoHktuX3du8laPVfO8\",\n  \"image\": \"https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052\",\n  \"name\": \"Main Theme From Trouble Man\",\n  \"uri\": \"spotify:track:6KUjwoHktuX3du8laPVfO8\",\n}\n`;\n\nexports[`getPreloadData > should handle playlist 1`] = `\n{\n  \"artists\": [\n    {\n      \"name\": \"Marvin Gaye\",\n      \"uri\": \"spotify:artist:3koiLjNrgRTNbOwViDipeA\",\n    },\n  ],\n  \"durationMs\": 151626,\n  \"id\": \"6KUjwoHktuX3du8laPVfO8\",\n  \"image\": \"https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052\",\n  \"name\": \"Main Theme From Trouble Man\",\n  \"uri\": \"spotify:track:6KUjwoHktuX3du8laPVfO8\",\n}\n`;\n\nexports[`getPreloadData > should handle show 1`] = `\n{\n  \"artists\": [\n    {\n      \"name\": \"Syntax - Tasty Web Development Treats\",\n      \"uri\": \"spotify:show:4kYCRYJ3yK5DQbP5tbfZby\",\n    },\n  ],\n  \"durationMs\": 2645609,\n  \"id\": \"5cTJgzcfadfcfPzvJAwJk1\",\n  \"image\": \"https://i.scdn.co/image/ab6765630000ba8ada4bfc6d17ba4b7f66e6012a\",\n  \"name\": \"846: Talking EVs: Range Anxiety, Charging, and Tech\",\n  \"uri\": \"spotify:episode:5cTJgzcfadfcfPzvJAwJk1\",\n}\n`;\n\nexports[`getPreloadData > should handle track 1`] = `\n{\n  \"artists\": [\n    {\n      \"name\": \"Marvin Gaye\",\n      \"uri\": \"spotify:artist:3koiLjNrgRTNbOwViDipeA\",\n    },\n  ],\n  \"durationMs\": 151626,\n  \"id\": \"6KUjwoHktuX3du8laPVfO8\",\n  \"image\": \"https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052\",\n  \"name\": \"Main Theme From Trouble Man\",\n  \"uri\": \"spotify:track:6KUjwoHktuX3du8laPVfO8\",\n}\n`;\n"
  },
  {
    "path": "test/modules/getters.spec.ts",
    "content": "import { TRANSPARENT_COLOR } from '~/constants';\nimport {\n  getBgColor,\n  getLocale,\n  getMergedStyles,\n  getPreloadData,\n  getSpotifyLink,\n  getSpotifyLinkTitle,\n  getSpotifyURIType,\n} from '~/modules/getters';\n\nimport {\n  playerAlbumTracks,\n  playerArtistTopTracks,\n  playerPlaylistTracks,\n  playerShow,\n  playerTrack,\n} from '../fixtures/data';\n\ndescribe('getBgColor', () => {\n  it('should return the background color', () => {\n    expect(getBgColor('#f04')).toBe('#f04');\n  });\n\n  it('should return the transparent color', () => {\n    expect(getBgColor('transparent')).toBe(TRANSPARENT_COLOR);\n  });\n});\n\ndescribe('getLocale', () => {\n  it('should return a merged locale', () => {\n    expect(getLocale({ currentDevice: 'Selected device ' })).toMatchSnapshot();\n  });\n});\n\ndescribe('getMergedStyles', () => {\n  it('should return a merged styles', () => {\n    expect(getMergedStyles({ bgColor: 'transparent', height: 100 })).toMatchSnapshot();\n  });\n});\n\ndescribe('getPreloadData', () => {\n  beforeAll(() => {\n    vi.spyOn(console, 'error').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  afterAll(() => {\n    vi.restoreAllMocks();\n  });\n\n  it('should handle albums', async () => {\n    fetchMock.mockResponseOnce(JSON.stringify(playerAlbumTracks));\n    fetchMock.mockResponseOnce(JSON.stringify(playerTrack));\n\n    await expect(\n      getPreloadData('token', 'spotify:album:7A0awCXkE1FtSU8B0qwOJQ', 0),\n    ).resolves.toMatchSnapshot();\n  });\n\n  it('should handle artist', async () => {\n    fetchMock.mockResponseOnce(JSON.stringify(playerArtistTopTracks));\n\n    await expect(\n      getPreloadData('token', 'spotify:artist:7A0awCXkE1FtSU8B0qwOJQ', 0),\n    ).resolves.toMatchSnapshot();\n  });\n\n  it('should handle playlist', async () => {\n    fetchMock.mockResponseOnce(JSON.stringify(playerPlaylistTracks));\n\n    await expect(\n      getPreloadData('token', 'spotify:playlist:7A0awCXkE1FtSU8B0qwOJQ', 0),\n    ).resolves.toMatchSnapshot();\n  });\n\n  it('should handle show', async () => {\n    fetchMock.mockResponseOnce(JSON.stringify(playerShow));\n    fetchMock.mockResponseOnce(JSON.stringify(playerShow.episodes));\n\n    await expect(\n      getPreloadData('token', 'spotify:show:7A0awCXkE1FtSU8B0qwOJQ', 0),\n    ).resolves.toMatchSnapshot();\n  });\n\n  it('should handle track', async () => {\n    fetchMock.mockResponseOnce(JSON.stringify(playerTrack));\n\n    await expect(\n      getPreloadData('token', 'spotify:track:7A0awCXkE1FtSU8B0qwOJQ', 0),\n    ).resolves.toMatchSnapshot();\n  });\n\n  it('should handle invalid type', async () => {\n    await expect(\n      getPreloadData('token', 'spotify:episode:7A0awCXkE1FtSU8B0qwOJQ', 0),\n    ).resolves.toBeNull();\n\n    expect(console.error).toHaveBeenCalledTimes(1);\n  });\n\n  it('should handle API errors', async () => {\n    fetchMock.mockRejectOnce(new Error('API error'));\n\n    await expect(\n      getPreloadData('token', 'spotify:track:7A0awCXkE1FtSU8B0qwOJQ', 0),\n    ).resolves.toBeNull();\n\n    expect(console.error).toHaveBeenCalledTimes(1);\n  });\n});\n\ndescribe('getSpotifyLink', () => {\n  it('should return a Spotify link', () => {\n    expect(getSpotifyLink('spotify:track:63DTXKZi7YdJ4tzGti1Dtr')).toBe(\n      'https://open.spotify.com/track/63DTXKZi7YdJ4tzGti1Dtr',\n    );\n  });\n});\n\ndescribe('getSpotifyLinkTitle', () => {\n  it('should return a formatted title', () => {\n    expect(getSpotifyLinkTitle('Hommer', getLocale().title)).toBe('Hommer on SPOTIFY');\n  });\n});\n\ndescribe('getSpotifyURIType', () => {\n  it.each([\n    ['spotify:album:51QBkcL7S3KYdXSSA0zM9R', 'album'],\n    ['spotify:artist:7A0awCXkE1FtSU8B0qwOJQ', 'artist'],\n    ['spotify:episode:6r8OOleI5xP7qCEipHvdyK', 'episode'],\n    ['spotify:playlist:5kHMGRfZHORA4UrCbhYyad', 'playlist'],\n    ['spotify:show:5huEzXsf133dhbh57Np2tg', 'show'],\n    ['spotify:track:0gkVD2tr14wCfJhqhdE94L', 'track'],\n    ['spotify:user:gilbarbara', 'user'],\n    ['spotify', ''],\n  ])('%s should return %s', (value, expected) => {\n    expect(getSpotifyURIType(value)).toBe(expected);\n  });\n});\n"
  },
  {
    "path": "test/modules/helpers.spec.ts",
    "content": "import {\n  isNumber,\n  loadSpotifyPlayer,\n  millisecondsToTime,\n  parseIds,\n  parseVolume,\n  round,\n  validateURI,\n} from '~/modules/helpers';\n\ndescribe('isNumber', () => {\n  it('should return properly', () => {\n    expect(isNumber(1)).toBeTrue();\n    expect(isNumber('1')).toBeFalse();\n  });\n});\n\ndescribe('loadSpotifyPlayer', () => {\n  afterAll(() => {\n    document.getElementById('spotify-player')?.remove();\n  });\n\n  it('should load the script', () => {\n    loadSpotifyPlayer();\n\n    const scriptTag = document.getElementById('spotify-player') as HTMLScriptElement;\n\n    expect(scriptTag.tagName).toBe('SCRIPT');\n    expect(scriptTag.src).toBe('https://sdk.scdn.co/spotify-player.js');\n  });\n});\n\ndescribe('millisecondsToTime', () => {\n  it.each([\n    [0, '0:00'],\n    [1200, '0:01'],\n    [63000, '1:03'],\n    [3610000, '01:00:10'],\n    [7123490, '01:58:43'],\n  ])('should convert %d to %s', (input, expected) => {\n    expect(millisecondsToTime(input)).toBe(expected);\n  });\n});\n\ndescribe('parseIds', () => {\n  it('should return properly', () => {\n    expect(parseIds('sek80pgtykoem9zr189zgyy9')).toEqual(['sek80pgtykoem9zr189zgyy9']);\n    expect(parseIds(['sek80pgtykoem9zr189zgyy9'])).toEqual(['sek80pgtykoem9zr189zgyy9']);\n    /* @ts-expect-error - missing parameter */\n    expect(parseIds()).toEqual([]);\n  });\n});\n\ndescribe('parseVolume', () => {\n  it.each([\n    [0.3, 0.3],\n    [1, 1],\n    ['100', 1],\n    [3, 0.03],\n    [20, 0.2],\n  ])('should parse %d to %d', (input, expected) => {\n    expect(parseVolume(input)).toBe(expected);\n  });\n});\n\ndescribe('round', () => {\n  it.each([\n    [10.1029, 1, 10.1],\n    [34.0293, 2, 34.03],\n    [79.0178, 3, 79.018],\n  ])('should convert %d with %d digits to %d', (input, digits, expected) => {\n    expect(round(input, digits)).toEqual(expected);\n  });\n});\n\ndescribe('validateURI', () => {\n  it.each([\n    ['spotify:album:51QBkcL7S3KYdXSSA0zM9R', true],\n    ['spotify:artist:7A0awCXkE1FtSU8B0qwOJQ', true],\n    ['spotify:episode:6r8OOleI5xP7qCEipHvdyK', false],\n    ['spotify:playlist:5kHMGRfZHORA4UrCbhYyad', true],\n    ['spotify:show:5huEzXsf133dhbh57Np2tg', true],\n    ['spotify:track:0gkVD2tr14wCfJhqhdE94L', true],\n    ['spotify:user:gilbarbara', false],\n  ])('%s should return %s', (value, expected) => {\n    expect(validateURI(value)).toBe(expected);\n  });\n});\n"
  },
  {
    "path": "test/modules/spotify.spec.ts",
    "content": "import {\n  checkTracksStatus,\n  getAlbumTracks,\n  getArtistTopTracks,\n  getDevices,\n  getPlaybackState,\n  getPlaylistTracks,\n  getQueue,\n  getShow,\n  getShowEpisodes,\n  getTrack,\n  next,\n  pause,\n  play,\n  previous,\n  removeTracks,\n  repeat,\n  saveTracks,\n  seek,\n  setDevice,\n  setVolume,\n  shuffle,\n} from '~/modules/spotify';\n\nimport {\n  playbackState,\n  playerAlbumTracks,\n  playerArtistTopTracks,\n  playerPlaylistTracks,\n  playerShow,\n  playerTrack,\n  queue,\n} from '../fixtures/data';\n\nconst deviceId = 'df17372ghs982js892js';\nconst token =\n  'BQDoGCFtLXDAVgphhrRSPFHmhG9ZND3BLzSE5WVE-2qoe7_YZzRcVtZ6F7qEhzTih45GyxZLhp9b53A1YAPObAgV0MDvsbcQg-gZzlrIeQwwsWnz3uulVvPMhqssNP5HnE5SX0P0wTOOta1vneq2dL4Hvdko5WqvRivrEKWXCvJTPAFStfa5V5iLdCSglg';\nconst id = '2ViHeieFA3iPmsBya2NDFl';\nconst trackUri = `spotify:track:${id}`;\n\nconst mockDevices = {\n  devices: [\n    {\n      id: deviceId,\n      name: 'Test Player',\n      type: 'Computer',\n    },\n  ],\n};\n\ndescribe('spotify', () => {\n  beforeAll(() => {\n    fetchMock.mockIf(/.*/, request => {\n      const { url } = request;\n\n      if (url.match(/contains\\?ids=*/)) {\n        return Promise.resolve({\n          body: JSON.stringify([false]),\n        });\n      } else if (url.match(/albums/)) {\n        return Promise.resolve({\n          body: JSON.stringify(playerAlbumTracks),\n        });\n      } else if (url.match(/artists/)) {\n        return Promise.resolve({\n          body: JSON.stringify(playerArtistTopTracks),\n        });\n      } else if (url.match(/playlist/)) {\n        return Promise.resolve({\n          body: JSON.stringify(playerPlaylistTracks),\n        });\n      } else if (url.match(/shows.*0/)) {\n        return Promise.resolve({\n          body: JSON.stringify(playerShow.episodes),\n        });\n      } else if (url.match(/shows.*/)) {\n        return Promise.resolve({\n          body: JSON.stringify(playerShow),\n        });\n      } else if (url.match(/tracks/)) {\n        return Promise.resolve({\n          body: JSON.stringify(playerTrack),\n        });\n      }\n\n      switch (url) {\n        case 'https://api.spotify.com/v1/me/player/devices': {\n          return Promise.resolve({\n            body: JSON.stringify(mockDevices),\n          });\n        }\n        case 'https://api.spotify.com/v1/me/player/queue': {\n          return Promise.resolve({\n            body: JSON.stringify(queue),\n          });\n        }\n        case 'https://api.spotify.com/v1/me/player': {\n          return Promise.resolve({\n            body: JSON.stringify(playbackState),\n          });\n        }\n        // No default\n      }\n\n      return Promise.resolve({\n        body: '',\n      });\n    });\n  });\n\n  it('checkTracksStatus', async () => {\n    await expect(checkTracksStatus(token, trackUri)).resolves.toEqual([false]);\n  });\n\n  it('getAlbumTracks', async () => {\n    await expect(getAlbumTracks(token, id)).resolves.toEqual(playerAlbumTracks);\n  });\n\n  it('getArtistTopTracks', async () => {\n    await expect(getArtistTopTracks(token, id)).resolves.toEqual(playerArtistTopTracks);\n  });\n\n  it('getDevices', async () => {\n    await expect(getDevices(token)).resolves.toEqual(mockDevices);\n  });\n\n  it('getPlaybackState', async () => {\n    await expect(getPlaybackState(token)).resolves.toEqual(playbackState);\n  });\n\n  it('getQueue', async () => {\n    await expect(getQueue(token)).resolves.toEqual(queue);\n  });\n\n  it('getPlaylistTracks', async () => {\n    await expect(getPlaylistTracks(token, id)).resolves.toEqual(playerPlaylistTracks);\n  });\n\n  it('getShow', async () => {\n    await expect(getShow(token, id)).resolves.toEqual(playerShow);\n  });\n\n  it('getShowEpisodes', async () => {\n    await expect(getShowEpisodes(token, id)).resolves.toEqual(playerShow.episodes);\n  });\n\n  it('getTrack', async () => {\n    await expect(getTrack(token, id)).resolves.toEqual(playerTrack);\n  });\n\n  it('pause', async () => {\n    await expect(pause(token)).resolves.toBeUndefined();\n  });\n\n  it('play', async () => {\n    await expect(play(token, { deviceId })).resolves.toBeUndefined();\n  });\n\n  it('previous', async () => {\n    await expect(previous(token)).resolves.toBeUndefined();\n  });\n\n  it('next', async () => {\n    await expect(next(token)).resolves.toBeUndefined();\n  });\n\n  it('removeTracks', async () => {\n    await expect(removeTracks(token, [trackUri])).resolves.toBeUndefined();\n  });\n\n  it('saveTracks', async () => {\n    await expect(saveTracks(token, [trackUri])).resolves.toBeUndefined();\n  });\n\n  it('repeat', async () => {\n    await expect(repeat(token, 'track')).resolves.toBeUndefined();\n  });\n\n  it('seek', async () => {\n    await expect(seek(token, 1029)).resolves.toBeUndefined();\n  });\n\n  it('setDevice', async () => {\n    await expect(setDevice(token, deviceId)).resolves.toBeUndefined();\n  });\n\n  it('setVolume', async () => {\n    await expect(setVolume(token, 1)).resolves.toBeUndefined();\n  });\n\n  it('shuffle', async () => {\n    await expect(shuffle(token, true)).resolves.toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "test/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig\",\n  \"compilerOptions\": {\n    \"noUnusedLocals\": false,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\"\n  },\n  \"include\": [\"**/*\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@gilbarbara/tsconfig\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"downlevelIteration\": true,\n    \"noEmit\": true,\n    \"paths\": {\n      \"~/*\": [\"src/*\"]\n    },\n    \"target\": \"ES2022\"\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "vitest.config.mts",
    "content": "import react from '@vitejs/plugin-react-swc';\nimport tsconfigPaths from 'vite-tsconfig-paths';\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  plugins: [tsconfigPaths(), react()],\n  test: {\n    include: ['test/**/*.spec.ts?(x)'],\n    coverage: {\n      all: true,\n      include: ['src/**/*.ts?(x)'],\n      exclude: [\n        'src/components/icons/DevicesMobile.tsx',\n        'src/components/icons/DevicesSpeaker.tsx',\n      ],\n      reporter: ['text', 'lcov'],\n      thresholds: {\n        statements: 90,\n        branches: 80,\n        functions: 90,\n        lines: 90,\n      },\n    },\n    environment: 'jsdom',\n    globals: true,\n    setupFiles: ['./test/__setup__/vitest.setup.ts'],\n  },\n});\n"
  }
]