[
  {
    "path": ".claude/settings.local.json",
    "content": "{\n  \"permissions\": {\n    \"allow\": [\n      \"Bash(pnpm build:*)\",\n      \"Bash(pnpm test:*)\",\n      \"Bash(npm test:*)\"\n    ],\n    \"deny\": []\n  }\n}"
  },
  {
    "path": ".cursorrules",
    "content": "- Use TypeScript.\n- Use function syntax for defining React components. Define the prop types inline.\n- If a value is exported, it should be exported on the same line as its definition.\n- Always define the return type of a function or component.\n- Use Tailwind CSS for styling.\n- Don't use trailing semicolons."
  },
  {
    "path": ".dockerignore",
    "content": ".DS_Store\n.next\nnode_modules\ndist\n.env"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: main\non:\n  push:\n    branches:\n      - main\njobs:\n  build:\n    name: Docker build, tag, and push\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Docker build, tag, and push\n      uses: pangzineng/Github-Action-One-Click-Docker@master\n      env:\n        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}\n        DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: tests\non: [push]\njobs:\n  tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 9\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'pnpm'\n      - run: pnpm install\n      - run: pnpm exec playwright install --with-deps\n      - run: pnpm lint:check\n      - run: pnpm format:check\n      - run: pnpm type:check\n      - run: pnpm test\n      - run: pnpm build\n      - run: pnpm test:e2e\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.next\nnode_modules\ndist\ntsconfig.tsbuildinfo\n.env"
  },
  {
    "path": ".prettierrc.js",
    "content": "'use strict';\n\nmodule.exports = {\n  semi: false,\n  trailingComma: 'all',\n  singleQuote: true,\n  printWidth: 80,\n  tabWidth: 2,\n};\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# FilePizza Development Guide\n\nA peer-to-peer file transfer application built with modern web technologies.\n\n## Prerequisites\n\n- [Node.js](https://nodejs.org/) (v18+)\n- [pnpm](https://pnpm.io/) (preferred package manager)\n\n## Quick Start\n\n```bash\ngit clone https://github.com/kern/filepizza.git\ncd filepizza\npnpm install\npnpm dev\n```\n\n## Available Commands\n\n### Development\n- `pnpm dev` - Start development server\n- `pnpm dev:full` - Start with Redis and COTURN for full WebRTC testing\n\n### Building & Testing\n- `pnpm build` - Build for production\n- `pnpm test` - Run unit tests with Vitest\n- `pnpm test:watch` - Run tests in watch mode\n- `pnpm test:e2e` - Run E2E tests with Playwright\n\n### Code Quality\n- `pnpm lint:check` - Check ESLint rules\n- `pnpm lint:fix` - Fix ESLint issues\n- `pnpm format` - Format code with Prettier\n- `pnpm format:check` - Check code formatting\n- `pnpm type:check` - TypeScript type checking\n\n### Docker\n- `pnpm docker:build` - Build Docker image\n- `pnpm docker:up` - Start containers\n- `pnpm docker:down` - Stop containers\n\n### CI Pipeline\n- `pnpm ci` - Run full CI pipeline (lint, format, type-check, test, build, e2e, docker)\n\n## Tech Stack\n\n- **Framework**: Next.js 15 with App Router\n- **UI**: React 19 + Tailwind CSS v4\n- **Language**: TypeScript\n- **Testing**: Vitest (unit) + Playwright (E2E)\n- **WebRTC**: PeerJS\n- **State Management**: TanStack Query\n- **Themes**: next-themes with View Transitions\n- **Storage**: Redis (optional)\n\n## Project Structure\n\n```\nsrc/\n├── app/                    # Next.js App Router pages\n├── components/             # React components\n├── hooks/                  # Custom React hooks\n├── utils/                  # Utility functions\n└── types.ts               # TypeScript definitions\n```\n\n## Development Tips\n\n### Using pnpm\n\nThis project uses pnpm as the package manager. Benefits include:\n- Faster installs and smaller disk usage\n- Strict dependency resolution\n- Built-in workspace support\n\nAlways use `pnpm` instead of `npm` or `yarn`:\n```bash\npnpm install package-name\npnpm remove package-name\npnpm update\n```\n\n### Code Style\n\n- ESLint + TypeScript ESLint for linting\n- Prettier for formatting\n- Husky + lint-staged for pre-commit hooks\n- Prefer TypeScript over JavaScript\n- Use kebab-case for files, PascalCase for components\n\n### Testing Strategy\n\n- Unit tests for components and utilities (`tests/unit/`)\n- E2E tests for critical user flows (`tests/e2e/`)\n- Test files follow `*.test.ts[x]` naming convention\n\n### WebRTC Development\n\nFor full WebRTC testing with TURN/STUN:\n```bash\npnpm dev:full\n```\n\nThis starts Redis and COTURN containers for testing peer connections behind NAT.\n\n## Key Dependencies\n\n- `next` - React framework\n- `tailwindcss` - CSS framework\n- `@tanstack/react-query` - Server state management\n- `peerjs` - WebRTC abstraction\n- `next-themes` - Theme switching\n- `zod` - Schema validation\n- `vitest` - Testing framework\n- `playwright` - E2E testing\n\nRun `pnpm ci` before submitting PRs to ensure all checks pass."
  },
  {
    "path": "Dockerfile",
    "content": "# Stage 1: Dependencies\nFROM node:lts-alpine AS deps\nRUN apk add --no-cache pnpm\nWORKDIR /app\nCOPY package.json pnpm-lock.yaml ./\n# Need all dependencies for build\nRUN pnpm install --frozen-lockfile\n\n# Stage 2: Builder\nFROM node:lts-alpine AS builder\nRUN apk add --no-cache pnpm\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\n# Builds standalone output\nRUN pnpm build\n\n# Stage 3: Runner\nFROM node:lts-alpine AS runner\nWORKDIR /app\n\nENV NODE_ENV production\nENV PORT 3000\n\n# Only copy standalone output - no need for node_modules\nCOPY --from=builder /app/public ./public\nCOPY --from=builder /app/.next/standalone ./\nCOPY --from=builder /app/.next/static ./.next/static\n\nUSER node\nEXPOSE 3000\n# Uses standalone server\nCMD [\"node\", \"server.js\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2015, Alex Kern\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n## SIL OPEN FONT LICENSE for fonts in static/fonts\n\nVersion 1.1 - 26 February 2007\nPREAMBLE\n\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded,\nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\nDEFINITIONS\n\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting — in part or in whole — any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\nPERMISSION & CONDITIONS\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\nTERMINATION\n\nThis license becomes null and void if any of the above conditions are\nnot met.\nDISCLAIMER\n\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://xkcd.com/949/\"><img src=\"http://imgs.xkcd.com/comics/file_transfer.png\" alt=\"XKCD 949\" width=\"30%\" align=\"right\" /></a> <img src=\"public/images/wordmark.png\" alt=\"FilePizza wordmark\" width=\"50%\" /> <h3>Peer-to-peer file transfers in your browser</h3>\n\n*Cooked up by [Alex Kern](https://kern.io) & [Neeraj Baid](https://github.com/neerajbaid) while eating Sliver @ UC Berkeley.*\n\nUsing [WebRTC](http://www.webrtc.org), FilePizza eliminates the initial upload step required by other web-based file sharing services. Because data is never stored in an intermediary server, the transfer is fast, private, and secure.\n\nA hosted instance of FilePizza is available at [file.pizza](https://file.pizza).\n\n## What's new with FilePizza v2\n\n* A new UI with dark mode support, now built on modern browser technologies.\n* Works on most mobile browsers, including Mobile Safari.\n* Transfers are now directly from the uploader to the downloader's browser (WebRTC without WebTorrent) with faster handshakes.\n* Uploaders can monitor the progress of the transfer and stop it if they want.\n* Better security and safety measures with password protection and reporting.\n* Support for uploading multiple files at once, which downloaders receive as a zip file.\n* Streaming downloads with a Service Worker.\n* Out-of-process storage of server state using Redis.\n\n## Development\n\n```\n$ git clone https://github.com/kern/filepizza.git\n$ pnpm install\n$ pnpm dev\n$ pnpm build\n$ pnpm start\n```\n\n## Running with Docker\n\n```\n$ pnpm docker:build\n$ pnpm docker:up\n$ pnpm docker:down\n```\n\n## Stack\n\n* Next.js\n* Tailwind\n* TypeScript\n* React\n* PeerJS for WebRTC\n* View Transitions\n* Redis (optional)\n\n## Configuration\n\nThe server can be customized with the following environment variables:\n\n- `REDIS_URL` – Connection string for a Redis instance used to store channel metadata. If not set, FilePizza falls back to in-memory storage.\n- `COTURN_ENABLED` – When set to `true`, enables TURN support for connecting peers behind NAT.\n- `TURN_HOST` – Hostname or IP address of the TURN server. Defaults to `127.0.0.1`.\n- `TURN_REALM` – Realm used when generating TURN credentials. Defaults to `file.pizza`.\n- `STUN_SERVER` – STUN server URL to use when `COTURN_ENABLED` is disabled. Defaults to `stun:stun.l.google.com:19302`.\n- `PEERJS_HOST` – Hostname or IP address to the self-hosted PeerJS server. Defaults to `0.peerjs.com`.\n- `PEERJS_PATH` – Path to self-hosted PeerJS server. Defaults to `/`.\n\n## FAQ\n\n**How are my files sent?** Your files are sent directly from your browser to the downloader's browser. They never pass through our servers. FilePizza uses WebRTC to send files. This requires that the uploader leave their browser window open until the transfer is complete.\n\n**Can multiple people download my file at once?** Yes! Just send them your short or long URL.\n\n**How big can my files be?** As big as your browser can handle.\n\n**What happens when I close my browser?** The URLs for your files will no longer work. If a downloader has completed the transfer, that downloader will continue to seed to incomplete downloaders, but no new downloads may be initiated.\n\n**Are my files encrypted?** Yes, all WebRTC communications are automatically encrypted using public-key cryptography because of DTLS. You can add an optional password to your upload for an extra layer of security.\n\n## License & Acknowledgements\n\nFilePizza is released under the [BSD 3-Clause license](https://github.com/kern/filepizza/blob/main/LICENSE). A huge thanks to [iblowyourdesign](https://dribbble.com/iblowyourdesign) for the pizza illustration.\n"
  },
  {
    "path": "bin/peerjs.js",
    "content": "#!/usr/bin/env node\nconst express = require('express')\nconst { ExpressPeerServer } = require('peer')\n\nconst app = express();\nconst server = app.listen(9000);\nconst peerServer = ExpressPeerServer(server, {\n  path: '/filepizza'\n})\n\napp.use('/peerjs', peerServer)\n"
  },
  {
    "path": "docker-compose.production.yml",
    "content": "services:\n  redis:\n    image: redis:latest\n    ports:\n      - 127.0.0.1:6379:6379\n    networks:\n      - filepizza\n    volumes:\n      - redis_data:/data\n  coturn:\n    image: coturn/coturn\n    ports:\n      - 3478:3478\n      - 3478:3478/udp\n      - 5349:5349\n      - 5349:5349/udp\n      - 60000-60128:60000-60128/udp\n    environment:\n      - DETECT_EXTERNAL_IP=yes\n      - DETECT_RELAY_IP=yes\n    command: -n --log-file=stdout --redis-userdb=\"ip=redis connect_timeout=30\" --min-port=60000 --max-port=60128\n    networks:\n      - filepizza\n  filepizza:\n    build: .\n    image: kern/filepizza:latest\n    ports:\n      - 0.0.0.0:80:80\n    environment:\n      - PORT=80\n      - REDIS_URL=redis://redis:6379\n      - COTURN_ENABLED=true\n    networks:\n      - filepizza\n    depends_on:\n      - redis\n    env_file:\n      - .env\n\nnetworks:\n  filepizza:\n    driver: bridge\n\nvolumes:\n  redis_data:\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  redis:\n    image: redis:latest\n    ports:\n      - 6379:6379\n    networks:\n      - filepizza\n    volumes:\n      - redis_data:/data\n  coturn:\n    image: coturn/coturn\n    ports:\n      - 3478:3478\n      - 3478:3478/udp\n      - 5349:5349\n      - 5349:5349/udp\n      # Relay Ports\n      # - 49152-65535:49152-65535/udp\n    environment:\n      - DETECT_EXTERNAL_IP=yes\n      - DETECT_RELAY_IP=yes\n    command: -n --log-file=stdout --redis-userdb=\"ip=redis connect_timeout=30\"\n    networks:\n      - filepizza\n  filepizza:\n    build: .\n    image: kern/filepizza:latest\n    ports:\n      - 8080:8080\n    environment:\n      - PORT=8080\n      - REDIS_URL=redis://redis:6379\n    networks:\n      - filepizza\n    depends_on:\n      - redis\n\nnetworks:\n  filepizza:\n    driver: bridge\n\nvolumes:\n  redis_data:\n"
  },
  {
    "path": "docs/file-transfer-protocol.md",
    "content": "# FilePizza File Transfer Protocol\n\nThis document explains the message-based protocol that FilePizza uses to\ntransfer files directly between browsers over a WebRTC data channel.  It\ncovers the complete conversation required to build either an uploader or a\ndownloader and includes examples for common scenarios.\n\n## Architecture Overview\n\n```mermaid\nflowchart LR\n    Uploader -- WebRTC / PeerJS --> Downloader\n    Uploader -- REST --> Server[(FilePizza Server)]\n    Downloader -- REST --> Server\n    Server -- signalling / slug --> Uploader\n    Server -- signalling / slug --> Downloader\n```\n\n1. The uploader creates a channel with the server and receives a slug that\n   encodes its PeerJS identifier.\n2. The downloader resolves the slug via the server to obtain the uploader's\n   PeerJS identifier.\n3. All subsequent messages travel directly between peers over a reliable\n   WebRTC data channel.\n\n## Message Types\n\nEvery message is a JSON object with a `type` field that matches one of the\nvalues in the table below.  Fields marked with `?` are optional.\n\n```mermaid\nclassDiagram\n    class RequestInfo {\n        +\"RequestInfo\" type\n        +string browserName\n        +string browserVersion\n        +string osName\n        +string osVersion\n        +string mobileVendor\n        +string mobileModel\n    }\n    class Info {\n        +\"Info\" type\n        +FileInfo[] files\n    }\n    class FileInfo {\n        +string fileName\n        +number size\n        +string type\n    }\n    class Start {\n        +\"Start\" type\n        +string fileName\n        +number offset\n    }\n    class Chunk {\n        +\"Chunk\" type\n        +string fileName\n        +number offset\n        +ArrayBuffer bytes\n        +boolean final\n    }\n    class ChunkAck {\n        +\"ChunkAck\" type\n        +string fileName\n        +number offset\n        +number bytesReceived\n    }\n    class Pause {\n        +\"Pause\" type\n    }\n    class Done {\n        +\"Done\" type\n    }\n    class Error {\n        +\"Error\" type\n        +string error\n    }\n    class PasswordRequired {\n        +\"PasswordRequired\" type\n        +string errorMessage?\n    }\n    class UsePassword {\n        +\"UsePassword\" type\n        +string password\n    }\n    class Report {\n        +\"Report\" type\n    }\n```\n\nChunks are sent in pieces of at most 256 KiB (`MAX_CHUNK_SIZE`). The `final` flag in a `Chunk` message marks the last piece of a file.\n\n## Normal Transfer Sequence\n\nThe following diagram shows the exchange for downloading multiple files\nwithout a password.\n\n```mermaid\nsequenceDiagram\n    participant D as Downloader\n    participant U as Uploader\n    D->>U: RequestInfo\n    U-->>D: Info(files)\n    loop For each file\n        D->>U: Start(fileName, offset=0)\n        loop For each chunk\n            U-->>D: Chunk(offset, bytes, final=false)\n            D->>U: ChunkAck(offset, bytesReceived)\n        end\n        U-->>D: Chunk(offset, bytes, final=true)\n        D->>U: ChunkAck(offset, bytesReceived)\n    end\n    D->>U: Done\n    U-->>D: close connection\n```\n\n## Password‑Protected Transfers\n\nIf the uploader specified a password when creating the channel, the\nconversation includes an authentication step.\n\n```mermaid\nsequenceDiagram\n    participant D as Downloader\n    participant U as Uploader\n    D->>U: RequestInfo\n    U-->>D: PasswordRequired(errorMessage?)\n    D->>U: UsePassword(password)\n    U-->>D: Info(files) or PasswordRequired(\"Invalid password\")\n    Note over D,U: Continue with normal transfer sequence on success\n```\n\n## Pause and Resume\n\nA downloader may pause an in‑progress transfer.  To resume, it reconnects and\nrequests the remainder of the file starting at the last acknowledged offset.\n\n```mermaid\nsequenceDiagram\n    participant D as Downloader\n    participant U as Uploader\n    D->>U: Start(fileName, offset=0)\n    U-->>D: Chunk(...)\n    D->>U: ChunkAck(...)\n    D->>U: Pause\n    Note over D,U: Connection closed or kept idle\n    D->>U: Start(fileName, offset=previouslyAcked)\n    Note over D,U: Transfer resumes from offset\n```\n\n## Reporting\n\nA special PeerJS connection with metadata `{ type: \"report\" }` causes the\nuploader to broadcast a `Report` message to all connected downloaders and to\nredirect its own UI to a reported page.  Downloaders receiving this message\nshould abort the transfer.\n\n```mermaid\nsequenceDiagram\n    participant Reporter\n    participant U as Uploader\n    participant D as Downloader\n    Reporter->>U: Peer connection(type=\"report\")\n    U-->>D: Report\n    U-->>Reporter: redirect to /reported\n```\n\n## Example Conversations\n\n### Single file without password\n\n```\nRequestInfo\nInfo [{ fileName: \"photo.jpg\", size: 1048576, type: \"image/jpeg\" }]\nStart { fileName: \"photo.jpg\", offset: 0 }\nChunk { offset: 0, bytes: <256 KB>, final: false }\nChunkAck { offset: 0, bytesReceived: 262144 }\n...\nChunk { offset: 1048576, bytes: <0>, final: true }\nChunkAck { offset: 1048576, bytesReceived: 0 }\nDone\n```\n\n### Password‑protected download\n\n```\nRequestInfo\nPasswordRequired\nUsePassword { password: \"secret\" }\nInfo [...]\n...\n```\n\n### Resuming after interruption\n\n```\nRequestInfo\nInfo [...]\nStart { fileName: \"video.mp4\", offset: 0 }\nChunk/ChunkAck exchanges...\n<connection drops after 1 MB>\nStart { fileName: \"video.mp4\", offset: 1048576 }\nChunk/ChunkAck exchanges...\nDone\n```\n\n---\n\nWith these message definitions and sequences you can implement a compatible\nuploader or downloader for FilePizza or adapt the protocol for other\napplications.\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "// @ts-check\n\nimport eslint from '@eslint/js';\nimport tseslint from 'typescript-eslint';\n\nexport default tseslint.config({\n  extends: [\n    eslint.configs.recommended,\n    tseslint.configs.recommended,\n  ],\n  rules: {\n    '@typescript-eslint/no-unused-vars': [\n      'error',\n      { argsIgnorePattern: '^_' },\n    ],\n    '@typescript-eslint/no-use-before-define': [\n      'error',\n      { variables: false },\n    ],\n    '@typescript-eslint/promise-function-async': 'off',\n    '@typescript-eslint/require-await': 'off',\n    '@typescript-eslint/no-explicit-any': 'warn',\n    'import/no-unused-modules': 'off',\n    'import/group-exports': 'off',\n    'import/no-extraneous-dependencies': 'off',\n    'new-cap': 'off',\n    'no-inline-comments': 'off',\n    'no-shadow': 'warn',\n    'no-use-before-define': 'off',\n  },\n  files: ['src/**/*.ts[x]'],\n  ignores: ['legacy', 'node_modules', '.next'],\n});\n"
  },
  {
    "path": "next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  // Disable strict mode to avoid calling useEffect twice in development.\n  // The uploader and downloader are both using useEffect to listen for peerjs events\n  // which causes the connection to be created twice.\n  reactStrictMode: false,\n  output: 'standalone'\n}\n\nmodule.exports = nextConfig"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"filepizza\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Free peer-to-peer file transfers in your browser.\",\n  \"author\": \"Alex Kern <alex@kern.io> (http://kern.io)\",\n  \"license\": \"BSD-3-Clause\",\n  \"homepage\": \"https://github.com/kern/filepizza\",\n  \"scripts\": {\n    \"dev\": \"next\",\n    \"dev:full\": \"docker compose up redis coturn -d && COTURN_ENABLED=true REDIS_URL=redis://localhost:6379 next\",\n    \"build\": \"next build && cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/\",\n    \"start\": \"next start\",\n    \"start:peerjs\": \"./bin/peerjs.js\",\n    \"lint:check\": \"eslint 'src/**/*.ts[x]'\",\n    \"lint:fix\": \"eslint 'src/**/*.ts[x]' --fix\",\n    \"docker:build\": \"docker compose build\",\n    \"docker:up\": \"docker compose up -d\",\n    \"docker:down\": \"docker compose down\",\n    \"docker:logs\": \"docker compose logs -f\",\n    \"docker:ps\": \"docker compose ps\",\n    \"docker:restart\": \"docker compose restart\",\n    \"docker:clean\": \"docker compose down -v --rmi all\",\n    \"format\": \"prettier --write \\\"src/**/*.{ts,tsx}\\\"\",\n    \"format:check\": \"prettier --check \\\"src/**/*.{ts,tsx}\\\"\",\n    \"type:check\": \"tsc --noEmit\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:e2e\": \"playwright test\",\n    \"ci\": \"pnpm lint:check && pnpm format:check && pnpm type:check && pnpm test && pnpm build && pnpm test:e2e && pnpm docker:build\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git@github.com:kern/filepizza.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/kern/filepizza/issues\"\n  },\n  \"dependencies\": {\n    \"@tailwindcss/postcss\": \"^4.1.11\",\n    \"@tanstack/react-query\": \"^5.55.2\",\n    \"autoprefixer\": \"^10.4.20\",\n    \"debug\": \"^4.3.6\",\n    \"express\": \"^5.0.0\",\n    \"ioredis\": \"^5.4.2\",\n    \"next\": \"~15.5.11\",\n    \"next-themes\": \"^0.4.4\",\n    \"next-view-transitions\": \"^0.3.4\",\n    \"nodemon\": \"^3.0.0\",\n    \"peer\": \"^1.0.0\",\n    \"peerjs\": \"^1.5.4\",\n    \"postcss\": \"^8.4.44\",\n    \"react\": \"~19.2.3\",\n    \"react-device-detect\": \"^2.0.0\",\n    \"react-dom\": \"~19.2.3\",\n    \"react-qr-code\": \"^2.0.15\",\n    \"streamsaver\": \"^2.0.6\",\n    \"tailwindcss\": \"^4.1.11\",\n    \"web-streams-polyfill\": \"^4.0.0\",\n    \"webrtcsupport\": \"^2.2.0\",\n    \"zod\": \"^4.0.0\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.30.0\",\n    \"@playwright/test\": \"^1.53.2\",\n    \"@testing-library/jest-dom\": \"^6.6.3\",\n    \"@testing-library/react\": \"^16.3.0\",\n    \"@testing-library/user-event\": \"^14.6.1\",\n    \"@types/debug\": \"^4.1.12\",\n    \"@types/node\": \"^22.10.2\",\n    \"@types/react\": \"^19.0.2\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.18.2\",\n    \"@typescript-eslint/parser\": \"^8.18.2\",\n    \"@vitejs/plugin-react\": \"^4.6.0\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.17.0\",\n    \"eslint-config-next\": \"~15.5.11\",\n    \"eslint-plugin-import\": \"^2.31.0\",\n    \"eslint-plugin-react\": \"^7.37.3\",\n    \"husky\": \"^9.0.0\",\n    \"jsdom\": \"^26.1.0\",\n    \"lint-staged\": \"^16.0.0\",\n    \"playwright\": \"^1.53.2\",\n    \"prettier\": \"^3.0.0\",\n    \"typescript\": \"^5.0.0\",\n    \"typescript-eslint\": \"^8.18.2\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"lint-staged\"\n    }\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx}\": [\n      \"eslint --fix\",\n      \"prettier --write\",\n      \"git add\"\n    ]\n  }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig } from '@playwright/test'\n\nexport default defineConfig({\n  testDir: './tests/e2e',\n  workers: 1, // Run tests serially to avoid WebRTC port conflicts\n  webServer: {\n    command: 'node .next/standalone/server.js',\n    url: 'http://localhost:3000',\n    timeout: 120 * 1000,\n    reuseExistingServer: true,\n  },\n  use: {\n    baseURL: 'http://localhost:3000',\n  },\n})\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    '@tailwindcss/postcss': {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "public/stream.html",
    "content": "<!--\n  https://github.com/jimmywarting/StreamSaver.js/blob/master/mitm.html\n\n\tmitm.html is the lite \"man in the middle\"\n\n\tThis is only meant to signal the opener's messageChannel to\n\tthe service worker - when that is done this mitm can be closed\n    but it's better to keep it alive since this also stops the sw\n    from restarting\n\n\tThe service worker is capable of intercepting all request and fork their\n\town \"fake\" response - wish we are going to craft\n\twhen the worker then receives a stream then the worker will tell the opener\n\tto open up a link that will start the download\n-->\n<script>\n// This will prevent the sw from restarting\nlet keepAlive = () => {\n  keepAlive = () => {}\n  var ping = location.href.substr(0, location.href.lastIndexOf('/')) + '/ping'\n  var interval = setInterval(() => {\n    if (sw) {\n      sw.postMessage('ping')\n    } else {\n      fetch(ping).then(res => res.text(!res.ok && clearInterval(interval)))\n    }\n  }, 10000)\n}\n\n// message event is the first thing we need to setup a listner for\n// don't want the opener to do a random timeout - instead they can listen for\n// the ready event\n// but since we need to wait for the Service Worker registration, we store the\n// message for later\nlet messages = []\nwindow.onmessage = evt => messages.push(evt)\n\nlet sw = null\nlet scope = ''\n\nfunction registerWorker() {\n  return navigator.serviceWorker.getRegistration('./').then(swReg => {\n    return swReg || navigator.serviceWorker.register('sw.js', { scope: './' })\n  }).then(swReg => {\n    const swRegTmp = swReg.installing || swReg.waiting\n\n    scope = swReg.scope\n\n    return (sw = swReg.active) || new Promise(resolve => {\n      swRegTmp.addEventListener('statechange', fn = () => {\n        if (swRegTmp.state === 'activated') {\n          swRegTmp.removeEventListener('statechange', fn)\n          sw = swReg.active\n          resolve()\n        }\n      })\n    })\n  })\n}\n\n// Now that we have the Service Worker registered we can process messages\nfunction onMessage (event) {\n  let { data, ports, origin } = event\n\n  // It's important to have a messageChannel, don't want to interfere\n  // with other simultaneous downloads\n  if (!ports || !ports.length) {\n    throw new TypeError(\"[StreamSaver] You didn't send a messageChannel\")\n  }\n\n  if (typeof data !== 'object') {\n    throw new TypeError(\"[StreamSaver] You didn't send a object\")\n  }\n\n  // the default public service worker for StreamSaver is shared among others.\n  // so all download links needs to be prefixed to avoid any other conflict\n  data.origin = origin\n\n  // if we ever (in some feature versoin of streamsaver) would like to\n  // redirect back to the page of who initiated a http request\n  data.referrer = data.referrer || document.referrer || origin\n\n  // pass along version for possible backwards compatibility in sw.js\n  data.streamSaverVersion = new URLSearchParams(location.search).get('version')\n\n  if (data.streamSaverVersion === '1.2.0') {\n    console.warn('[StreamSaver] please update streamsaver')\n  }\n\n  /** @since v2.0.0 */\n  if (!data.headers) {\n    console.warn(\"[StreamSaver] pass `data.headers` that you would like to pass along to the service worker\\nit should be a 2D array or a key/val object that fetch's Headers api accepts\")\n  } else {\n    // test if it's correct\n    // should thorw a typeError if not\n    new Headers(data.headers)\n  }\n\n  /** @since v2.0.0 */\n  if (typeof data.filename === 'string') {\n    console.warn(\"[StreamSaver] You shouldn't send `data.filename` anymore. It should be included in the Content-Disposition header option\")\n    // Do what File constructor do with fileNames\n    data.filename = data.filename.replace(/\\//g, ':')\n  }\n\n  /** @since v2.0.0 */\n  if (data.size) {\n    console.warn(\"[StreamSaver] You shouldn't send `data.size` anymore. It should be included in the content-length header option\")\n  }\n\n  /** @since v2.0.0 */\n  if (data.readableStream) {\n    console.warn(\"[StreamSaver] You should send the readableStream in the messageChannel, not throught mitm\")\n  }\n\n  /** @since v2.0.0 */\n  if (!data.pathname) {\n    console.warn(\"[StreamSaver] Please send `data.pathname` (eg: /pictures/summer.jpg)\")\n    data.pathname = Math.random().toString().slice(-6) + '/' + data.filename\n  }\n\n  // remove all leading slashes\n  data.pathname = data.pathname.replace(/^\\/+/g, '')\n\n  // remove protocol\n  let org = origin.replace(/(^\\w+:|^)\\/\\//, '')\n\n  // set the absolute pathname to the download url.\n  data.url = new URL(`${scope + org}/${data.pathname}`).toString()\n\n  if (!data.url.startsWith(`${scope + org}/`)) {\n    throw new TypeError('[StreamSaver] bad `data.pathname`')\n  }\n\n  // This sends the message data as well as transferring\n  // messageChannel.port2 to the service worker. The service worker can\n  // then use the transferred port to reply via postMessage(), which\n  // will in turn trigger the onmessage handler on messageChannel.port1.\n\n  const transferable = data.readableStream\n    ? [ ports[0], data.readableStream ]\n    : [ ports[0] ]\n\n  if (!(data.readableStream || data.transferringReadable)) {\n    keepAlive()\n  }\n\n  return sw.postMessage(data, transferable)\n}\n\nif (window.opener) {\n  // The opener can't listen to onload event, so we need to help em out!\n  // (telling them that we are ready to accept postMessage's)\n  window.opener.postMessage('StreamSaver::loadedPopup', '*')\n}\n\nif (navigator.serviceWorker) {\n  registerWorker().then(() => {\n    window.onmessage = onMessage\n    messages.forEach(window.onmessage)\n  })\n} else {\n  // FF can ping sw with fetch from a secure hidden iframe\n  // shouldn't really be possible?\n  keepAlive()\n}\n\n</script>\n"
  },
  {
    "path": "public/sw.js",
    "content": "// https://github.com/jimmywarting/StreamSaver.js/blob/master/sw.js\n\n/* global self ReadableStream Response */\n\nself.addEventListener('install', () => {\n  self.skipWaiting()\n})\n\nself.addEventListener('activate', (event) => {\n  event.waitUntil(self.clients.claim())\n})\n\nconst map = new Map()\n\n// This should be called once per download\n// Each event has a dataChannel that the data will be piped through\nself.onmessage = (event) => {\n  // We send a heartbeat every x secound to keep the\n  // service worker alive if a transferable stream is not sent\n  if (event.data === 'ping') {\n    return\n  }\n\n  const data = event.data\n  const downloadUrl =\n    data.url ||\n    self.registration.scope +\n      Math.random() +\n      '/' +\n      (typeof data === 'string' ? data : data.filename)\n  const port = event.ports[0]\n  const metadata = new Array(3) // [stream, data, port]\n\n  metadata[1] = data\n  metadata[2] = port\n\n  // Note to self:\n  // old streamsaver v1.2.0 might still use `readableStream`...\n  // but v2.0.0 will always transfer the stream throught MessageChannel #94\n  if (event.data.readableStream) {\n    metadata[0] = event.data.readableStream\n  } else if (event.data.transferringReadable) {\n    port.onmessage = (evt) => {\n      port.onmessage = null\n      metadata[0] = evt.data.readableStream\n    }\n  } else {\n    metadata[0] = createStream(port)\n  }\n\n  map.set(downloadUrl, metadata)\n  port.postMessage({ download: downloadUrl })\n}\n\nfunction createStream(port) {\n  // ReadableStream is only supported by chrome 52\n  return new ReadableStream({\n    start(controller) {\n      // When we receive data on the messageChannel, we write\n      port.onmessage = ({ data }) => {\n        if (data === 'end') {\n          return controller.close()\n        }\n\n        if (data === 'abort') {\n          controller.error('Aborted the download')\n          return\n        }\n\n        controller.enqueue(data)\n      }\n    },\n    cancel() {\n      console.log('user aborted')\n    },\n  })\n}\n\nself.onfetch = (event) => {\n  const url = event.request.url\n\n  // this only works for Firefox\n  if (url.endsWith('/ping')) {\n    return event.respondWith(new Response('pong'))\n  }\n\n  const hijacke = map.get(url)\n\n  if (!hijacke) return null\n\n  const [stream, data, port] = hijacke\n\n  map.delete(url)\n\n  // Not comfortable letting any user control all headers\n  // so we only copy over the length & disposition\n  const responseHeaders = new Headers({\n    'Content-Type': 'application/octet-stream; charset=utf-8',\n\n    // To be on the safe side, The link can be opened in a iframe.\n    // but octet-stream should stop it.\n    'Content-Security-Policy': \"default-src 'none'\",\n    'X-Content-Security-Policy': \"default-src 'none'\",\n    'X-WebKit-CSP': \"default-src 'none'\",\n    'X-XSS-Protection': '1; mode=block',\n  })\n\n  let headers = new Headers(data.headers || {})\n\n  if (headers.has('Content-Length')) {\n    responseHeaders.set('Content-Length', headers.get('Content-Length'))\n  }\n\n  if (headers.has('Content-Disposition')) {\n    responseHeaders.set(\n      'Content-Disposition',\n      headers.get('Content-Disposition'),\n    )\n  }\n\n  // data, data.filename and size should not be used anymore\n  if (data.size) {\n    console.warn('Depricated')\n    responseHeaders.set('Content-Length', data.size)\n  }\n\n  let fileName = typeof data === 'string' ? data : data.filename\n  if (fileName) {\n    console.warn('Depricated')\n    // Make filename RFC5987 compatible\n    fileName = encodeURIComponent(fileName)\n      .replace(/['()]/g, escape)\n      .replace(/\\*/g, '%2A')\n    responseHeaders.set(\n      'Content-Disposition',\n      \"attachment; filename*=UTF-8''\" + fileName,\n    )\n  }\n\n  event.respondWith(new Response(stream, { headers: responseHeaders }))\n\n  port.postMessage({ debug: 'Download started' })\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:recommended\"\n  ]\n}\n"
  },
  {
    "path": "scripts/pull-and-run.sh",
    "content": "#!/bin/bash\n\nset -euo pipefail\n\ngit pull origin main\nsudo docker pull kern/filepizza:latest\nsudo docker compose -f docker-compose.production.yml up -d\nsudo docker compose logs -f"
  },
  {
    "path": "src/app/api/create/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { getOrCreateChannelRepo } from '../../../channel'\n\nexport async function POST(request: Request): Promise<NextResponse> {\n  const { uploaderPeerID } = await request.json()\n\n  if (!uploaderPeerID) {\n    return NextResponse.json(\n      { error: 'Uploader peer ID is required' },\n      { status: 400 },\n    )\n  }\n\n  const channel = await getOrCreateChannelRepo().createChannel(uploaderPeerID)\n  return NextResponse.json(channel)\n}\n"
  },
  {
    "path": "src/app/api/destroy/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { getOrCreateChannelRepo } from '../../../channel'\n\nexport async function POST(request: NextRequest): Promise<NextResponse> {\n  const { slug } = await request.json()\n\n  if (!slug) {\n    return NextResponse.json({ error: 'Slug is required' }, { status: 400 })\n  }\n\n  // Anyone can destroy a channel if they know the slug. This enables a terms violation reporter to destroy the channel after they report it.\n\n  try {\n    await getOrCreateChannelRepo().destroyChannel(slug)\n    return NextResponse.json({ success: true }, { status: 200 })\n  } catch (error) {\n    console.error(error)\n    return NextResponse.json(\n      { error: 'Failed to destroy channel' },\n      { status: 500 },\n    )\n  }\n}\n"
  },
  {
    "path": "src/app/api/ice/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport crypto from 'crypto'\nimport { setTurnCredentials } from '../../../coturn'\n\nconst turnHost = process.env.TURN_HOST || '127.0.0.1'\nconst stunServer = process.env.STUN_SERVER || 'stun:stun.l.google.com:19302'\nconst peerjsHost = process.env.PEERJS_HOST || '0.peerjs.com'\nconst peerjsPath = process.env.PEERJS_PATH || '/'\n\nexport async function POST(): Promise<NextResponse> {\n  if (!process.env.COTURN_ENABLED) {\n    return NextResponse.json({\n      host: peerjsHost,\n      path: peerjsPath,\n      iceServers: [{ urls: stunServer }],\n    })\n  }\n\n  // Generate ephemeral credentials\n  const username = crypto.randomBytes(8).toString('hex')\n  const password = crypto.randomBytes(8).toString('hex')\n  const ttl = 86400 // 24 hours\n\n  // Store credentials in Redis\n  await setTurnCredentials(username, password, ttl)\n\n  return NextResponse.json({\n    host: peerjsHost,\n    path: peerjsPath,\n    iceServers: [\n      { urls: stunServer },\n      {\n        urls: [`turn:${turnHost}:3478`, `turns:${turnHost}:5349`],\n        username,\n        credential: password,\n      },\n    ],\n  })\n}\n"
  },
  {
    "path": "src/app/api/renew/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { getOrCreateChannelRepo } from '../../../channel'\n\nexport async function POST(request: NextRequest): Promise<NextResponse> {\n  const { slug, secret } = await request.json()\n\n  if (!slug) {\n    return NextResponse.json({ error: 'Slug is required' }, { status: 400 })\n  }\n\n  if (!secret) {\n    return NextResponse.json({ error: 'Secret is required' }, { status: 400 })\n  }\n\n  const success = await getOrCreateChannelRepo().renewChannel(slug, secret)\n  return NextResponse.json({ success })\n}\n"
  },
  {
    "path": "src/app/download/[...slug]/page.tsx",
    "content": "import { JSX } from 'react'\nimport { notFound } from 'next/navigation'\nimport { getOrCreateChannelRepo } from '../../../channel'\nimport Spinner from '../../../components/Spinner'\nimport Wordmark from '../../../components/Wordmark'\nimport Downloader from '../../../components/Downloader'\nimport WebRTCPeerProvider from '../../../components/WebRTCProvider'\nimport ReportTermsViolationButton from '../../../components/ReportTermsViolationButton'\n\nconst normalizeSlug = (rawSlug: string | string[]): string => {\n  if (typeof rawSlug === 'string') {\n    return rawSlug\n  } else {\n    return rawSlug.join('/')\n  }\n}\n\nexport default async function DownloadPage({\n  params,\n}: {\n  params: Promise<{ slug: string[] }>\n}): Promise<JSX.Element> {\n  const { slug: slugRaw } = await params\n  const slug = normalizeSlug(slugRaw)\n  const channel = await getOrCreateChannelRepo().fetchChannel(slug)\n\n  if (!channel) {\n    notFound()\n  }\n\n  return (\n    <div className=\"flex flex-col items-center space-y-5 py-10 max-w-2xl mx-auto\">\n      <Spinner direction=\"down\" />\n      <Wordmark />\n      <WebRTCPeerProvider>\n        <Downloader uploaderPeerID={channel.uploaderPeerID} />\n        <ReportTermsViolationButton\n          uploaderPeerID={channel.uploaderPeerID}\n          slug={slug}\n        />\n      </WebRTCPeerProvider>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import React from 'react'\nimport Footer from '../components/Footer'\nimport '../styles.css'\nimport { ThemeProvider } from '../components/ThemeProvider'\nimport { ModeToggle } from '../components/ModeToggle'\nimport FilePizzaQueryClientProvider from '../components/QueryClientProvider'\nimport { Viewport } from 'next'\nimport { ViewTransitions } from 'next-view-transitions'\n\nexport const metadata = {\n  title: 'FilePizza • Your files, delivered.',\n  description: 'Peer-to-peer file transfers in your web browser.',\n  charSet: 'utf-8',\n  openGraph: {\n    url: 'https://file.pizza',\n    title: 'FilePizza • Your files, delivered.',\n    description: 'Peer-to-peer file transfers in your web browser.',\n    images: [{ url: 'https://file.pizza/images/fb.png' }],\n  },\n}\n\nexport const viewport: Viewport = {\n  width: 'device-width',\n  initialScale: 1,\n  maximumScale: 1,\n  userScalable: false,\n}\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode\n}): React.ReactElement {\n  return (\n    <ViewTransitions>\n      <html lang=\"en\" suppressHydrationWarning>\n        <body>\n          <ThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem>\n            <FilePizzaQueryClientProvider>\n              <main>{children}</main>\n              <Footer />\n              <ModeToggle />\n            </FilePizzaQueryClientProvider>\n          </ThemeProvider>\n        </body>\n      </html>\n    </ViewTransitions>\n  )\n}\n"
  },
  {
    "path": "src/app/not-found.tsx",
    "content": "import { JSX } from 'react'\nimport Spinner from '../components/Spinner'\nimport Wordmark from '../components/Wordmark'\nimport ReturnHome from '../components/ReturnHome'\nimport TitleText from '../components/TitleText'\n\nexport const metadata = {\n  title: 'FilePizza - 404: Slice Not Found',\n  description: 'Oops! This slice of FilePizza seems to be missing.',\n}\n\nexport default async function NotFound(): Promise<JSX.Element> {\n  return (\n    <div className=\"flex flex-col items-center space-y-5 py-10 max-w-2xl mx-auto\">\n      <Spinner direction=\"down\" />\n      <Wordmark />\n      <TitleText>404: Looks like this slice of FilePizza got eaten!</TitleText>\n      <ReturnHome />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/app/page.tsx",
    "content": "'use client'\n\nimport React, { JSX, useCallback, useState } from 'react'\nimport WebRTCPeerProvider from '../components/WebRTCProvider'\nimport DropZone from '../components/DropZone'\nimport UploadFileList from '../components/UploadFileList'\nimport Uploader from '../components/Uploader'\nimport PasswordField from '../components/PasswordField'\nimport StartButton from '../components/StartButton'\nimport { UploadedFile } from '../types'\nimport Spinner from '../components/Spinner'\nimport Wordmark from '../components/Wordmark'\nimport CancelButton from '../components/CancelButton'\nimport { useMemo } from 'react'\nimport { getFileName } from '../fs'\nimport TitleText from '../components/TitleText'\nimport SubtitleText from '../components/SubtitleText'\nimport { pluralize } from '../utils/pluralize'\nimport TermsAcceptance from '../components/TermsAcceptance'\nimport AddFilesButton from '../components/AddFilesButton'\n\nfunction PageWrapper({ children }: { children: React.ReactNode }): JSX.Element {\n  return (\n    <div className=\"flex flex-col items-center space-y-5 py-10 max-w-2xl mx-auto px-4\">\n      <Spinner direction=\"up\" />\n      <Wordmark />\n      {children}\n    </div>\n  )\n}\n\nfunction InitialState({\n  onDrop,\n}: {\n  onDrop: (files: UploadedFile[]) => void\n}): JSX.Element {\n  return (\n    <PageWrapper>\n      <div className=\"flex flex-col items-center space-y-1 max-w-md\">\n        <TitleText>Peer-to-peer file transfers in your browser.</TitleText>\n      </div>\n      <DropZone onDrop={onDrop} />\n      <TermsAcceptance />\n    </PageWrapper>\n  )\n}\n\nfunction useUploaderFileListData(uploadedFiles: UploadedFile[]) {\n  return useMemo(() => {\n    return uploadedFiles.map((item) => ({\n      fileName: getFileName(item),\n      type: item.type,\n    }))\n  }, [uploadedFiles])\n}\n\nfunction ConfirmUploadState({\n  uploadedFiles,\n  password,\n  onChangePassword,\n  onCancel,\n  onStart,\n  onRemoveFile,\n  onAddFiles,\n}: {\n  uploadedFiles: UploadedFile[]\n  password: string\n  onChangePassword: (pw: string) => void\n  onCancel: () => void\n  onStart: () => void\n  onRemoveFile: (index: number) => void\n  onAddFiles: (files: UploadedFile[]) => void\n}): JSX.Element {\n  const fileListData = useUploaderFileListData(uploadedFiles)\n  return (\n    <PageWrapper>\n      <TitleText>\n        You are about to start uploading{' '}\n        {pluralize(uploadedFiles.length, 'file', 'files')}.{' '}\n        <AddFilesButton onAdd={onAddFiles} />\n      </TitleText>\n      <UploadFileList files={fileListData} onRemove={onRemoveFile} />\n      <PasswordField value={password} onChange={onChangePassword} />\n      <div className=\"flex space-x-4\">\n        <CancelButton onClick={onCancel} />\n        <StartButton onClick={onStart} />\n      </div>\n    </PageWrapper>\n  )\n}\n\nfunction UploadingState({\n  uploadedFiles,\n  password,\n  onStop,\n}: {\n  uploadedFiles: UploadedFile[]\n  password: string\n  onStop: () => void\n}): JSX.Element {\n  const fileListData = useUploaderFileListData(uploadedFiles)\n  return (\n    <PageWrapper>\n      <TitleText>\n        You are uploading {pluralize(uploadedFiles.length, 'file', 'files')}.\n      </TitleText>\n      <SubtitleText>\n        Leave this tab open. FilePizza does not store files.\n      </SubtitleText>\n      <UploadFileList files={fileListData} />\n      <WebRTCPeerProvider>\n        <Uploader files={uploadedFiles} password={password} onStop={onStop} />\n      </WebRTCPeerProvider>\n    </PageWrapper>\n  )\n}\n\nexport default function UploadPage(): JSX.Element {\n  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])\n  const [password, setPassword] = useState('')\n  const [uploading, setUploading] = useState(false)\n\n  const handleDrop = useCallback((files: UploadedFile[]): void => {\n    setUploadedFiles(files)\n  }, [])\n\n  const handleChangePassword = useCallback((pw: string) => {\n    setPassword(pw)\n  }, [])\n\n  const handleStart = useCallback(() => {\n    setUploading(true)\n  }, [])\n\n  const handleStop = useCallback(() => {\n    setUploading(false)\n  }, [])\n\n  const handleCancel = useCallback(() => {\n    setUploadedFiles([])\n    setUploading(false)\n  }, [])\n\n  const handleRemoveFile = useCallback((index: number) => {\n    setUploadedFiles((fs) => fs.filter((_, i) => i !== index))\n  }, [])\n\n  const handleAddFiles = useCallback((files: UploadedFile[]) => {\n    setUploadedFiles((fs) => [...fs, ...files])\n  }, [])\n\n  if (!uploadedFiles.length) {\n    return <InitialState onDrop={handleDrop} />\n  }\n\n  if (!uploading) {\n    return (\n      <ConfirmUploadState\n        uploadedFiles={uploadedFiles}\n        password={password}\n        onChangePassword={handleChangePassword}\n        onCancel={handleCancel}\n        onStart={handleStart}\n        onRemoveFile={handleRemoveFile}\n        onAddFiles={handleAddFiles}\n      />\n    )\n  }\n\n  return (\n    <UploadingState\n      uploadedFiles={uploadedFiles}\n      password={password}\n      onStop={handleStop}\n    />\n  )\n}\n"
  },
  {
    "path": "src/app/reported/page.tsx",
    "content": "import { JSX } from 'react'\nimport Spinner from '../../components/Spinner'\nimport Wordmark from '../../components/Wordmark'\nimport TitleText from '../../components/TitleText'\nimport ReturnHome from '../../components/ReturnHome'\n\nexport default function ReportedPage(): JSX.Element {\n  return (\n    <div className=\"flex flex-col items-center space-y-5 py-10 max-w-md mx-auto\">\n      <Spinner direction=\"down\" />\n      <Wordmark />\n\n      <TitleText>This delivery has been halted.</TitleText>\n      <div className=\"px-8 py-6 bg-stone-100 dark:bg-stone-800 rounded-lg border border-stone-200 dark:border-stone-700\">\n        <h3 className=\"text-lg font-medium text-stone-800 dark:text-stone-200 mb-4\">\n          Message from the management\n        </h3>\n        <p className=\"text-sm text-stone-600 dark:text-stone-300 leading-relaxed mb-6\">\n          Just like a pizza with questionable toppings, we've had to put this\n          delivery on hold for potential violations of our terms of service. Our\n          delivery quality team is looking into it to ensure we maintain our\n          high standards.\n        </p>\n        <div className=\"text-sm text-stone-500 dark:text-stone-400 italic\">\n          - The FilePizza Team\n        </div>\n      </div>\n\n      <ReturnHome />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/channel.ts",
    "content": "import 'server-only'\nimport config from './config'\nimport { Redis, getRedisClient } from './redisClient'\nimport { generateShortSlug, generateLongSlug } from './slugs'\nimport crypto from 'crypto'\nimport { z } from 'zod'\n\nexport type Channel = {\n  secret?: string\n  longSlug: string\n  shortSlug: string\n  uploaderPeerID: string\n}\n\nconst ChannelSchema = z.object({\n  secret: z.string().optional(),\n  longSlug: z.string(),\n  shortSlug: z.string(),\n  uploaderPeerID: z.string(),\n})\n\nexport interface ChannelRepo {\n  createChannel(uploaderPeerID: string, ttl?: number): Promise<Channel>\n  fetchChannel(slug: string): Promise<Channel | null>\n  renewChannel(slug: string, secret: string, ttl?: number): Promise<boolean>\n  destroyChannel(slug: string): Promise<void>\n}\n\nfunction getShortSlugKey(shortSlug: string): string {\n  return `short:${shortSlug}`\n}\n\nfunction getLongSlugKey(longSlug: string): string {\n  return `long:${longSlug}`\n}\n\nasync function generateShortSlugUntilUnique(\n  checkExists: (key: string) => Promise<boolean>,\n): Promise<string> {\n  for (let i = 0; i < config.shortSlug.maxAttempts; i++) {\n    const slug = await generateShortSlug()\n    const exists = await checkExists(getShortSlugKey(slug))\n    if (!exists) {\n      return slug\n    }\n  }\n\n  throw new Error('max attempts reached generating short slug')\n}\n\nasync function generateLongSlugUntilUnique(\n  checkExists: (key: string) => Promise<boolean>,\n): Promise<string> {\n  for (let i = 0; i < config.longSlug.maxAttempts; i++) {\n    const slug = await generateLongSlug()\n    const exists = await checkExists(getLongSlugKey(slug))\n    if (!exists) {\n      return slug\n    }\n  }\n\n  throw new Error('max attempts reached generating long slug')\n}\n\nfunction serializeChannel(channel: Channel): string {\n  return JSON.stringify(channel)\n}\n\nfunction deserializeChannel(str: string, scrubSecret = false): Channel {\n  const parsedChannel = JSON.parse(str)\n  const validatedChannel = ChannelSchema.parse(parsedChannel)\n  if (scrubSecret) {\n    return { ...validatedChannel, secret: undefined }\n  }\n  return validatedChannel\n}\n\ntype MemoryStoredChannel = {\n  channel: Channel\n  expiresAt: number\n}\n\nexport class MemoryChannelRepo implements ChannelRepo {\n  private channels: Map<string, MemoryStoredChannel> = new Map()\n  private timeouts: Map<string, NodeJS.Timeout> = new Map()\n\n  private setChannelTimeout(slug: string, ttl: number) {\n    // Clear any existing timeout\n    const existingTimeout = this.timeouts.get(slug)\n    if (existingTimeout) {\n      clearTimeout(existingTimeout)\n    }\n\n    // Set new timeout to remove channel when expired\n    const timeout = setTimeout(() => {\n      this.channels.delete(slug)\n      this.timeouts.delete(slug)\n    }, ttl * 1000)\n\n    this.timeouts.set(slug, timeout)\n  }\n\n  async createChannel(\n    uploaderPeerID: string,\n    ttl: number = config.channel.ttl,\n  ): Promise<Channel> {\n    const shortSlug = await generateShortSlugUntilUnique(async (key) =>\n      this.channels.has(key),\n    )\n    const longSlug = await generateLongSlugUntilUnique(async (key) =>\n      this.channels.has(key),\n    )\n\n    const channel: Channel = {\n      secret: crypto.randomUUID(),\n      longSlug,\n      shortSlug,\n      uploaderPeerID,\n    }\n\n    const expiresAt = Date.now() + ttl * 1000\n    const storedChannel = { channel, expiresAt }\n\n    const shortKey = getShortSlugKey(shortSlug)\n    const longKey = getLongSlugKey(longSlug)\n\n    this.channels.set(shortKey, storedChannel)\n    this.channels.set(longKey, storedChannel)\n\n    this.setChannelTimeout(shortKey, ttl)\n    this.setChannelTimeout(longKey, ttl)\n\n    return channel\n  }\n\n  async fetchChannel(\n    slug: string,\n    scrubSecret = false,\n  ): Promise<Channel | null> {\n    const shortKey = getShortSlugKey(slug)\n    const shortChannel = this.channels.get(shortKey)\n    if (shortChannel) {\n      return scrubSecret\n        ? { ...shortChannel.channel, secret: undefined }\n        : shortChannel.channel\n    }\n\n    const longKey = getLongSlugKey(slug)\n    const longChannel = this.channels.get(longKey)\n    if (longChannel) {\n      return scrubSecret\n        ? { ...longChannel.channel, secret: undefined }\n        : longChannel.channel\n    }\n\n    return null\n  }\n\n  async renewChannel(\n    slug: string,\n    secret: string,\n    ttl: number = config.channel.ttl,\n  ): Promise<boolean> {\n    const channel = await this.fetchChannel(slug)\n    if (!channel || channel.secret !== secret) {\n      return false\n    }\n\n    const expiresAt = Date.now() + ttl * 1000\n    const storedChannel = { channel, expiresAt }\n\n    const shortKey = getShortSlugKey(channel.shortSlug)\n    const longKey = getLongSlugKey(channel.longSlug)\n\n    this.channels.set(longKey, storedChannel)\n    this.channels.set(shortKey, storedChannel)\n\n    this.setChannelTimeout(shortKey, ttl)\n    this.setChannelTimeout(longKey, ttl)\n\n    return true\n  }\n\n  async destroyChannel(slug: string): Promise<void> {\n    const channel = await this.fetchChannel(slug)\n    if (!channel) {\n      return\n    }\n\n    const shortKey = getShortSlugKey(channel.shortSlug)\n    const longKey = getLongSlugKey(channel.longSlug)\n\n    // Clear timeouts\n    const shortTimeout = this.timeouts.get(shortKey)\n    if (shortTimeout) {\n      clearTimeout(shortTimeout)\n      this.timeouts.delete(shortKey)\n    }\n\n    const longTimeout = this.timeouts.get(longKey)\n    if (longTimeout) {\n      clearTimeout(longTimeout)\n      this.timeouts.delete(longKey)\n    }\n\n    this.channels.delete(longKey)\n    this.channels.delete(shortKey)\n  }\n}\n\nexport class RedisChannelRepo implements ChannelRepo {\n  client: Redis\n\n  constructor() {\n    this.client = getRedisClient()\n  }\n\n  async createChannel(\n    uploaderPeerID: string,\n    ttl: number = config.channel.ttl,\n  ): Promise<Channel> {\n    const shortSlug = await generateShortSlugUntilUnique(\n      async (key) => (await this.client.get(key)) !== null,\n    )\n    const longSlug = await generateLongSlugUntilUnique(\n      async (key) => (await this.client.get(key)) !== null,\n    )\n\n    const channel: Channel = {\n      secret: crypto.randomUUID(),\n      longSlug,\n      shortSlug,\n      uploaderPeerID,\n    }\n    const channelStr = serializeChannel(channel)\n\n    await this.client.setex(getLongSlugKey(longSlug), ttl, channelStr)\n    await this.client.setex(getShortSlugKey(shortSlug), ttl, channelStr)\n\n    return channel\n  }\n\n  async fetchChannel(\n    slug: string,\n    scrubSecret = false,\n  ): Promise<Channel | null> {\n    const shortChannelStr = await this.client.get(getShortSlugKey(slug))\n    if (shortChannelStr) {\n      return deserializeChannel(shortChannelStr, scrubSecret)\n    }\n\n    const longChannelStr = await this.client.get(getLongSlugKey(slug))\n    if (longChannelStr) {\n      return deserializeChannel(longChannelStr, scrubSecret)\n    }\n\n    return null\n  }\n\n  async renewChannel(\n    slug: string,\n    secret: string,\n    ttl: number = config.channel.ttl,\n  ): Promise<boolean> {\n    const channel = await this.fetchChannel(slug)\n    if (!channel || channel.secret !== secret) {\n      return false\n    }\n\n    await this.client.expire(getLongSlugKey(channel.longSlug), ttl)\n    await this.client.expire(getShortSlugKey(channel.shortSlug), ttl)\n\n    return true\n  }\n\n  async destroyChannel(slug: string): Promise<void> {\n    const channel = await this.fetchChannel(slug)\n    if (!channel) {\n      return\n    }\n\n    await this.client.del(getLongSlugKey(channel.longSlug))\n    await this.client.del(getShortSlugKey(channel.shortSlug))\n  }\n}\n\nlet _channelRepo: ChannelRepo | null = null\n\nexport function getOrCreateChannelRepo(): ChannelRepo {\n  if (!_channelRepo) {\n    if (process.env.REDIS_URL) {\n      _channelRepo = new RedisChannelRepo()\n      console.log('[ChannelRepo] Using Redis storage')\n    } else {\n      _channelRepo = new MemoryChannelRepo()\n      console.log('[ChannelRepo] Using in-memory storage')\n    }\n  }\n  return _channelRepo\n}\n"
  },
  {
    "path": "src/components/AddFilesButton.tsx",
    "content": "import React, { useRef, useCallback, JSX } from 'react'\nimport { UploadedFile } from '../types'\n\nexport default function AddFilesButton({\n  onAdd,\n}: {\n  onAdd: (files: UploadedFile[]) => void\n}): JSX.Element {\n  const fileInputRef = useRef<HTMLInputElement>(null)\n\n  const handleClick = useCallback(() => {\n    fileInputRef.current?.click()\n  }, [])\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      if (e.target.files) {\n        onAdd(Array.from(e.target.files) as UploadedFile[])\n        e.target.value = ''\n      }\n    },\n    [onAdd],\n  )\n\n  return (\n    <>\n      <input\n        id=\"add-files-input\"\n        type=\"file\"\n        ref={fileInputRef}\n        className=\"hidden\"\n        multiple\n        onChange={handleChange}\n      />\n      <button\n        id=\"add-files-button\"\n        type=\"button\"\n        onClick={handleClick}\n        className=\"underline text-stone-700 dark:text-stone-300 hover:text-stone-900 dark:hover:text-stone-100 transition-colors duration-200\"\n      >\n        Add more files\n      </button>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/CancelButton.tsx",
    "content": "import React, { JSX } from 'react'\n\nexport default function CancelButton({\n  onClick,\n  text = 'Cancel',\n}: {\n  onClick: React.MouseEventHandler\n  text?: string\n}): JSX.Element {\n  return (\n    <button\n      onClick={onClick}\n      className=\"px-4 py-2 text-sm font-medium text-stone-700 dark:text-stone-200 bg-white dark:bg-stone-800 border border-stone-300 dark:border-stone-600 rounded-md hover:bg-stone-50 dark:hover:bg-stone-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-indigo-400\"\n    >\n      {text}\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/ConnectionListItem.tsx",
    "content": "import React, { JSX } from 'react'\nimport { UploaderConnection, UploaderConnectionStatus } from '../types'\nimport ProgressBar from './ProgressBar'\n\nexport function ConnectionListItem({\n  conn,\n}: {\n  conn: UploaderConnection\n}): JSX.Element {\n  const getStatusColor = (status: UploaderConnectionStatus) => {\n    switch (status) {\n      case UploaderConnectionStatus.Uploading:\n        return 'bg-blue-500 dark:bg-blue-600'\n      case UploaderConnectionStatus.Paused:\n        return 'bg-yellow-500 dark:bg-yellow-600'\n      case UploaderConnectionStatus.Done:\n        return 'bg-green-500 dark:bg-green-600'\n      case UploaderConnectionStatus.Closed:\n        return 'bg-red-500 dark:bg-red-600'\n      case UploaderConnectionStatus.InvalidPassword:\n        return 'bg-red-500 dark:bg-red-600'\n      default:\n        return 'bg-stone-500 dark:bg-stone-600'\n    }\n  }\n\n  return (\n    <div className=\"w-full mt-4\">\n      <div className=\"flex justify-between items-center mb-2\">\n        <div className=\"flex items-center space-x-2\">\n          <span className=\"text-sm font-medium\">\n            {conn.browserName && conn.browserVersion ? (\n              <>\n                {conn.browserName}{' '}\n                <span className=\"text-stone-400\">v{conn.browserVersion}</span>\n              </>\n            ) : (\n              'Downloader'\n            )}\n          </span>\n          <span\n            className={`px-1.5 py-0.5 text-white rounded-md transition-colors duration-200 font-medium text-[10px] ${getStatusColor(\n              conn.status,\n            )}`}\n          >\n            {conn.status.replace(/_/g, ' ')}\n          </span>\n        </div>\n\n        <div className=\"text-sm text-stone-500 dark:text-stone-400\">\n          <div>\n            Completed: {conn.completedFiles} / {conn.totalFiles} files\n          </div>\n          {conn.uploadingFileName &&\n            conn.status === UploaderConnectionStatus.Uploading && (\n              <div>\n                Current file: {Math.round(conn.currentFileProgress * 100)}%\n              </div>\n            )}\n        </div>\n      </div>\n      <ProgressBar\n        value={\n          conn.completedFiles === conn.totalFiles\n            ? 1\n            : (conn.completedFiles + conn.currentFileProgress) / conn.totalFiles\n        }\n        max={1}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/CopyableInput.tsx",
    "content": "import React, { JSX } from 'react'\nimport useClipboard from '../hooks/useClipboard'\nimport InputLabel from './InputLabel'\n\nexport function CopyableInput({\n  label,\n  value,\n}: {\n  label: string\n  value: string\n}): JSX.Element {\n  const { hasCopied, onCopy } = useClipboard(value)\n\n  return (\n    <div className=\"flex flex-col w-full\">\n      <InputLabel>{label}</InputLabel>\n      <div className=\"flex w-full\">\n        <input\n          id={`copyable-input-${label.toLowerCase().replace(/\\s+/g, '-')}`}\n          className=\"grow px-3 py-2 text-xs border border-r-0 rounded-l text-stone-900 dark:text-stone-100 bg-white dark:bg-stone-800 border-stone-300 dark:border-stone-600\"\n          value={value}\n          readOnly\n        />\n        <button\n          className=\"px-4 py-2 text-sm text-stone-700 dark:text-stone-200 bg-stone-100 dark:bg-stone-700 hover:bg-stone-200 dark:hover:bg-stone-600 rounded-r border-t border-r border-b border-stone-300 dark:border-stone-600\"\n          onClick={onCopy}\n        >\n          {hasCopied ? 'Copied' : 'Copy'}\n        </button>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/DownloadButton.tsx",
    "content": "import React, { JSX } from 'react'\n\nexport default function DownloadButton({\n  onClick,\n}: {\n  onClick?: React.MouseEventHandler\n}): JSX.Element {\n  return (\n    <button\n      id=\"download-button\"\n      onClick={onClick}\n      className=\"h-12 px-4 bg-linear-to-b from-green-500 to-green-600 text-white rounded-md hover:from-green-500 hover:to-green-700 transition-all duration-200 border border-green-600 shadow-sm hover:shadow-md text-shadow\"\n    >\n      Download\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/Downloader.tsx",
    "content": "'use client'\n\nimport React, { JSX, useState, useCallback, useEffect } from 'react'\nimport { useDownloader } from '../hooks/useDownloader'\nimport PasswordField from './PasswordField'\nimport UnlockButton from './UnlockButton'\nimport Loading from './Loading'\nimport UploadFileList from './UploadFileList'\nimport DownloadButton from './DownloadButton'\nimport StopButton from './StopButton'\nimport ProgressBar from './ProgressBar'\nimport TitleText from './TitleText'\nimport ReturnHome from './ReturnHome'\nimport { pluralize } from '../utils/pluralize'\nimport { ErrorMessage } from './ErrorMessage'\n\ninterface FileInfo {\n  fileName: string\n  size: number\n  type: string\n}\n\nexport function ConnectingToUploader({\n  showTroubleshootingAfter = 3000,\n}: {\n  showTroubleshootingAfter?: number\n}): JSX.Element {\n  const [showTroubleshooting, setShowTroubleshooting] = useState(false)\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      setShowTroubleshooting(true)\n    }, showTroubleshootingAfter)\n    return () => clearTimeout(timer)\n  }, [showTroubleshootingAfter])\n\n  if (!showTroubleshooting) {\n    return <Loading text=\"Connecting to uploader...\" />\n  }\n\n  return (\n    <>\n      <Loading text=\"Connecting to uploader...\" />\n\n      <div className=\"bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-700 rounded-lg p-8 max-w-md w-full\">\n        <h2 className=\"text-xl font-bold mb-4 text-stone-900 dark:text-stone-50\">\n          Having trouble connecting?\n        </h2>\n\n        <div className=\"space-y-4 text-stone-700 dark:text-stone-300\">\n          <p>\n            FilePizza uses direct peer-to-peer connections, but sometimes the\n            connection can get stuck. Here are some possible reasons this can\n            happen:\n          </p>\n\n          <ul className=\"list-none space-y-3\">\n            <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n              <span className=\"text-base\">🚪</span>\n              <span className=\"text-sm\">\n                The uploader may have closed their browser, lost connectivity,\n                or stopped the upload. FilePizza requires the uploader to stay\n                online continuously because files are transferred directly\n                between browsers.\n              </span>\n            </li>\n            <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n              <span className=\"text-base\">🔒</span>\n              <span className=\"text-sm\">\n                Your network might have strict firewalls or NAT settings, such\n                as having UPnP disabled\n              </span>\n            </li>\n            <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n              <span className=\"text-base\">🌐</span>\n              <span className=\"text-sm\">\n                Some corporate or school networks block peer-to-peer connections\n              </span>\n            </li>\n          </ul>\n        </div>\n      </div>\n      <ReturnHome />\n    </>\n  )\n}\n\nexport function DownloadComplete({\n  filesInfo,\n  bytesDownloaded,\n  totalSize,\n}: {\n  filesInfo: FileInfo[]\n  bytesDownloaded: number\n  totalSize: number\n}): JSX.Element {\n  return (\n    <>\n      <TitleText>\n        You downloaded {pluralize(filesInfo.length, 'file', 'files')}.\n      </TitleText>\n      <div className=\"flex flex-col space-y-5 w-full\">\n        <UploadFileList files={filesInfo} />\n        <div className=\"w-full\">\n          <ProgressBar value={bytesDownloaded} max={totalSize} />\n        </div>\n        <ReturnHome />\n      </div>\n    </>\n  )\n}\n\nexport function DownloadInProgress({\n  filesInfo,\n  bytesDownloaded,\n  totalSize,\n  onStop,\n}: {\n  filesInfo: FileInfo[]\n  bytesDownloaded: number\n  totalSize: number\n  onStop: () => void\n}): JSX.Element {\n  return (\n    <>\n      <TitleText>\n        You are downloading {pluralize(filesInfo.length, 'file', 'files')}.\n      </TitleText>\n      <div className=\"flex flex-col space-y-5 w-full\">\n        <UploadFileList files={filesInfo} />\n        <div className=\"w-full\">\n          <ProgressBar value={bytesDownloaded} max={totalSize} />\n        </div>\n        <div className=\"flex justify-center w-full\">\n          <StopButton onClick={onStop} isDownloading />\n        </div>\n      </div>\n    </>\n  )\n}\n\nexport function ReadyToDownload({\n  filesInfo,\n  onStart,\n}: {\n  filesInfo: FileInfo[]\n  onStart: () => void\n}): JSX.Element {\n  return (\n    <>\n      <TitleText>\n        You are about to start downloading{' '}\n        {pluralize(filesInfo.length, 'file', 'files')}.\n      </TitleText>\n      <div className=\"flex flex-col space-y-5 w-full\">\n        <UploadFileList files={filesInfo} />\n        <DownloadButton onClick={onStart} />\n      </div>\n    </>\n  )\n}\n\nexport function PasswordEntry({\n  onSubmit,\n  errorMessage,\n}: {\n  onSubmit: (password: string) => void\n  errorMessage: string | null\n}): JSX.Element {\n  const [password, setPassword] = useState('')\n  const handleSubmit = useCallback(\n    (e: React.FormEvent) => {\n      e.preventDefault()\n      onSubmit(password)\n    },\n    [onSubmit, password],\n  )\n\n  return (\n    <>\n      <TitleText>This download requires a password.</TitleText>\n      <div className=\"flex flex-col space-y-5 w-full\">\n        <form\n          action=\"#\"\n          method=\"post\"\n          onSubmit={handleSubmit}\n          className=\"w-full\"\n        >\n          <div className=\"flex flex-col space-y-5 w-full\">\n            <PasswordField\n              value={password}\n              onChange={setPassword}\n              isRequired\n              isInvalid={Boolean(errorMessage)}\n            />\n            <UnlockButton />\n          </div>\n        </form>\n      </div>\n      {errorMessage && <ErrorMessage message={errorMessage} />}\n    </>\n  )\n}\n\nexport default function Downloader({\n  uploaderPeerID,\n}: {\n  uploaderPeerID: string\n}): JSX.Element {\n  const {\n    filesInfo,\n    isConnected,\n    isPasswordRequired,\n    isDownloading,\n    isDone,\n    errorMessage,\n    submitPassword,\n    startDownload,\n    stopDownload,\n    totalSize,\n    bytesDownloaded,\n  } = useDownloader(uploaderPeerID)\n\n  if (isDone && filesInfo) {\n    return (\n      <DownloadComplete\n        filesInfo={filesInfo}\n        bytesDownloaded={bytesDownloaded}\n        totalSize={totalSize}\n      />\n    )\n  }\n\n  if (isPasswordRequired) {\n    return (\n      <PasswordEntry errorMessage={errorMessage} onSubmit={submitPassword} />\n    )\n  }\n\n  if (errorMessage) {\n    return (\n      <>\n        <ErrorMessage message={errorMessage} />\n        <ReturnHome />\n      </>\n    )\n  }\n\n  if (isDownloading && filesInfo) {\n    return (\n      <DownloadInProgress\n        filesInfo={filesInfo}\n        bytesDownloaded={bytesDownloaded}\n        totalSize={totalSize}\n        onStop={stopDownload}\n      />\n    )\n  }\n\n  if (filesInfo) {\n    return <ReadyToDownload filesInfo={filesInfo} onStart={startDownload} />\n  }\n\n  if (!isConnected) {\n    return <ConnectingToUploader />\n  }\n\n  return <Loading text=\"Uh oh... Something went wrong.\" />\n}\n"
  },
  {
    "path": "src/components/DropZone.tsx",
    "content": "import React, { JSX, useState, useCallback, useEffect, useRef } from 'react'\nimport { extractFileList } from '../fs'\n\nexport default function DropZone({\n  onDrop,\n}: {\n  onDrop: (files: File[]) => void\n}): JSX.Element {\n  const [isDragging, setIsDragging] = useState(false)\n  const [fileCount, setFileCount] = useState(0)\n  const fileInputRef = useRef<HTMLInputElement>(null)\n\n  const handleDragEnter = useCallback((e: DragEvent) => {\n    e.preventDefault()\n    setIsDragging(true)\n    setFileCount(e.dataTransfer?.items.length || 0)\n  }, [])\n\n  const handleDragLeave = useCallback((e: DragEvent) => {\n    e.preventDefault()\n\n    const currentTarget =\n      e.currentTarget === window ? window.document : e.currentTarget\n    if (\n      e.relatedTarget &&\n      currentTarget instanceof Node &&\n      currentTarget.contains(e.relatedTarget as Node)\n    ) {\n      return\n    }\n\n    setIsDragging(false)\n  }, [])\n\n  const handleDragOver = useCallback((e: DragEvent) => {\n    e.preventDefault()\n    if (e.dataTransfer) {\n      e.dataTransfer.dropEffect = 'copy'\n    }\n  }, [])\n\n  const handleDrop = useCallback(\n    async (e: DragEvent) => {\n      e.preventDefault()\n      setIsDragging(false)\n\n      if (e.dataTransfer) {\n        const files = await extractFileList(e)\n        onDrop(files)\n      }\n    },\n    [onDrop],\n  )\n\n  const handleClick = useCallback(() => {\n    fileInputRef.current?.click()\n  }, [])\n\n  const handleFileInputChange = useCallback(\n    async (e: React.ChangeEvent<HTMLInputElement>) => {\n      if (e.target.files) {\n        const files = Array.from(e.target.files)\n        onDrop(files)\n      }\n    },\n    [onDrop],\n  )\n\n  useEffect(() => {\n    window.addEventListener('dragenter', handleDragEnter)\n    window.addEventListener('dragleave', handleDragLeave)\n    window.addEventListener('dragover', handleDragOver)\n    window.addEventListener('drop', handleDrop)\n\n    return () => {\n      window.removeEventListener('dragenter', handleDragEnter)\n      window.removeEventListener('dragleave', handleDragLeave)\n      window.removeEventListener('dragover', handleDragOver)\n      window.removeEventListener('drop', handleDrop)\n    }\n  }, [handleDragEnter, handleDragLeave, handleDragOver, handleDrop])\n\n  return (\n    <>\n      <div\n        className={`fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center text-2xl text-white transition-opacity duration-300 backdrop-blur-sm z-50 ${\n          isDragging ? 'opacity-100 visible' : 'opacity-0 invisible'\n        }`}\n      >\n        Drop to select {fileCount} file{fileCount !== 1 ? 's' : ''}\n      </div>\n      <input\n        type=\"file\"\n        ref={fileInputRef}\n        className=\"hidden\"\n        onChange={handleFileInputChange}\n        multiple\n      />\n      <button\n        id=\"drop-zone-button\"\n        className=\"block cursor-pointer relative py-3 px-6 text-base font-bold text-stone-700 dark:text-stone-200 bg-white dark:bg-stone-800 border-2 border-stone-700 dark:border-stone-700 rounded-lg transition-all duration-300 ease-in-out outline-none hover:shadow-md active:shadow-inner focus:shadow-outline\"\n        onClick={handleClick}\n      >\n        <span className=\"text-center text-stone-700 dark:text-stone-200\">\n          Drop a file to get started\n        </span>\n      </button>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/ErrorMessage.tsx",
    "content": "import { JSX } from 'react'\n\nexport function ErrorMessage({ message }: { message: string }): JSX.Element {\n  return (\n    <div\n      className=\"bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded relative\"\n      role=\"alert\"\n    >\n      <span className=\"block sm:inline\">{message}</span>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Footer.tsx",
    "content": "'use client'\n\nimport React, { JSX, useCallback } from 'react'\n\nconst DONATE_HREF =\n  'https://commerce.coinbase.com/checkout/247b6ffe-fb4e-47a8-9a76-e6b7ef83ea22'\n\nfunction FooterLink({\n  href,\n  children,\n}: {\n  href: string\n  children: React.ReactNode\n}): JSX.Element {\n  return (\n    <a\n      className=\"text-stone-600 dark:text-stone-400 underline hover:text-stone-800 dark:hover:text-stone-200\"\n      href={href}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n    >\n      {children}\n    </a>\n  )\n}\n\nexport function Footer(): JSX.Element {\n  const handleDonate = useCallback(() => {\n    window.location.href = DONATE_HREF\n  }, [])\n\n  return (\n    <>\n      <div className=\"h-[100px]\" /> {/* Spacer to account for footer height */}\n      <footer className=\"fixed bottom-0 left-0 right-0 text-center py-2.5 pb-4 text-xs border-t border-stone-200 dark:border-stone-700 shadow-[0_-1px_2px_rgba(0,0,0,0.04)] dark:shadow-[0_-1px_2px_rgba(255,255,255,0.04)] bg-white dark:bg-stone-900\">\n        <div className=\"flex flex-col items-center space-y-1 px-4 sm:px-6 md:px-8\">\n          <div className=\"flex items-center space-x-2\">\n            <p className=\"text-stone-600 dark:text-stone-400\">\n              <strong>Like FilePizza v2?</strong> Support its development!{' '}\n            </p>\n            <button\n              className=\"px-1.5 py-0.5 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200 font-medium text-[10px]\"\n              onClick={handleDonate}\n            >\n              Donate\n            </button>\n          </div>\n\n          <p className=\"text-stone-600 dark:text-stone-400\">\n            Cooked up by{' '}\n            <FooterLink href=\"http://kern.io\">Alex Kern</FooterLink> &amp;{' '}\n            <FooterLink href=\"https://github.com/neerajbaid\">\n              Neeraj Baid\n            </FooterLink>{' '}\n            while eating <strong>Sliver</strong> @ UC Berkeley &middot;{' '}\n            <FooterLink href=\"https://github.com/kern/filepizza#faq\">\n              FAQ\n            </FooterLink>{' '}\n            &middot;{' '}\n            <FooterLink href=\"https://github.com/kern/filepizza\">\n              Fork us\n            </FooterLink>\n          </p>\n        </div>\n      </footer>\n    </>\n  )\n}\n\nexport default Footer\n"
  },
  {
    "path": "src/components/InputLabel.tsx",
    "content": "import React, { JSX } from 'react'\n\nexport default function InputLabel({\n  children,\n  hasError = false,\n  tooltip,\n}: {\n  children: React.ReactNode\n  hasError?: boolean\n  tooltip?: string\n}): JSX.Element {\n  return (\n    <div className=\"relative flex items-center gap-1\">\n      <label\n        className={`text-[10px] mb-0.5 font-bold group relative inline-block ${\n          hasError ? 'text-red-500' : 'text-stone-400'\n        }`}\n      >\n        {children}\n      </label>\n      {tooltip && (\n        <div className=\"relative\">\n          <div\n            className=\"text-[11px] text-stone-400 dark:text-stone-400 cursor-help hover:opacity-80 peer focus:opacity-80\"\n            role=\"button\"\n            aria-label=\"Show tooltip\"\n            tabIndex={0}\n          >\n            ⓘ\n          </div>\n          <div className=\"pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-1 opacity-0 peer-hover:opacity-100 peer-focus:opacity-100 transition-opacity duration-200 z-10\">\n            <div className=\"bg-stone-100 dark:bg-stone-800 text-stone-800 dark:text-stone-100 text-xs rounded px-3 py-2 w-[320px] border border-stone-200 dark:border-stone-700 shadow-lg\">\n              {tooltip}\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Loading.tsx",
    "content": "import React, { JSX } from 'react'\n\nexport default function Loading({ text }: { text: string }): JSX.Element {\n  return (\n    <div className=\"flex flex-col items-center\">\n      <p className=\"text-sm text-stone-600 dark:text-stone-400 mt-2\">{text}</p>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/ModeToggle.tsx",
    "content": "'use client'\n\nimport { useTheme } from 'next-themes'\nimport { JSX } from 'react'\nfunction LightModeIcon(): JSX.Element {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      strokeWidth={1.5}\n      stroke=\"currentColor\"\n      className=\"w-4 h-4 block dark:hidden\"\n    >\n      <path\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        d=\"M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z\"\n      />\n    </svg>\n  )\n}\n\nfunction DarkModeIcon(): JSX.Element {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      strokeWidth={1.5}\n      stroke=\"currentColor\"\n      className=\"w-4 h-4 hidden dark:block\"\n    >\n      <path\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        d=\"M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z\"\n      />\n    </svg>\n  )\n}\n\nexport function ModeToggle(): JSX.Element {\n  const { setTheme, resolvedTheme } = useTheme()\n\n  return (\n    <button\n      onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}\n      className=\"fixed top-4 right-4 border rounded-md w-6 h-6 flex items-center justify-center\"\n    >\n      <span className=\"sr-only\">Toggle mode</span>\n      <LightModeIcon />\n      <DarkModeIcon />\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/PasswordField.tsx",
    "content": "import React, { JSX, useCallback } from 'react'\nimport InputLabel from './InputLabel'\n\nexport default function PasswordField({\n  value,\n  onChange,\n  isRequired = false,\n  isInvalid = false,\n}: {\n  value: string\n  onChange: (v: string) => void\n  isRequired?: boolean\n  isInvalid?: boolean\n}): JSX.Element {\n  const handleChange = useCallback(\n    function (e: React.ChangeEvent<HTMLInputElement>): void {\n      onChange(e.target.value)\n    },\n    [onChange],\n  )\n\n  return (\n    <div className=\"flex flex-col w-full\">\n      <InputLabel\n        hasError={isInvalid}\n        tooltip=\"The downloader must provide this password to start downloading the file. If you don't specify a password here, any downloader with the link to the file will be able to download it. It is not used to encrypt the file, as this is performed by WebRTC's DTLS already.\"\n      >\n        {isRequired ? 'Password' : 'Password (optional)'}\n      </InputLabel>\n      <input\n        autoFocus\n        type=\"password\"\n        className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 ${\n          isInvalid\n            ? 'border-red-500 dark:border-red-400'\n            : 'border-stone-300 dark:border-stone-600'\n        } bg-white dark:bg-stone-800 text-stone-900 dark:text-stone-100`}\n        placeholder=\"Enter a secret password for this slice of FilePizza...\"\n        value={value}\n        onChange={handleChange}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/ProgressBar.tsx",
    "content": "import React, { JSX } from 'react'\n\nexport default function ProgressBar({\n  value,\n  max,\n}: {\n  value: number\n  max: number\n}): JSX.Element {\n  const percentage = (value / max) * 100\n  const isComplete = value === max\n\n  return (\n    <div\n      id=\"progress-bar\"\n      className=\"w-full h-12 bg-stone-200 dark:bg-stone-700 rounded-md overflow-hidden relative shadow-sm\"\n    >\n      <div className=\"absolute inset-0 flex items-center justify-center\">\n        <span className=\"text-black font-bold\">{Math.round(percentage)}%</span>\n      </div>\n      <div\n        id=\"progress-bar-fill\"\n        className={`h-full ${\n          isComplete\n            ? 'bg-linear-to-b from-green-500 to-green-600'\n            : 'bg-linear-to-b from-blue-500 to-blue-600'\n        } transition-all duration-300 ease-in-out`}\n        style={{ width: `${percentage}%` }}\n      />\n      <div className=\"absolute inset-0 flex items-center justify-center\">\n        <span\n          id=\"progress-percentage\"\n          className=\"text-white font-bold text-shadow\"\n        >\n          {Math.round(percentage)}%\n        </span>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/QueryClientProvider.tsx",
    "content": "'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\n\nconst queryClient = new QueryClient()\n\nexport default function FilePizzaQueryClientProvider({\n  children,\n}: {\n  children: React.ReactNode\n}): React.ReactElement {\n  return (\n    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n  )\n}\n"
  },
  {
    "path": "src/components/ReportTermsViolationButton.tsx",
    "content": "'use client'\n\nimport { JSX } from 'react'\nimport { useWebRTCPeer } from './WebRTCProvider'\nimport { useCallback, useState } from 'react'\nimport { useMutation } from '@tanstack/react-query'\nimport CancelButton from './CancelButton'\n\nexport default function ReportTermsViolationButton({\n  uploaderPeerID,\n  slug,\n}: {\n  uploaderPeerID: string\n  slug: string\n}): JSX.Element {\n  const { peer } = useWebRTCPeer()\n  const [showModal, setShowModal] = useState(false)\n  const [isReporting, setIsReporting] = useState(false)\n\n  const reportMutation = useMutation({\n    mutationFn: async () => {\n      const response = await fetch(`/api/destroy`, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ slug }),\n      })\n      if (!response.ok) {\n        throw new Error('Failed to report violation')\n      }\n      return response.json()\n    },\n  })\n\n  const handleReport = useCallback(() => {\n    try {\n      // Destroy the channel so no further downloads can be made.\n      setIsReporting(true)\n      reportMutation.mutate()\n\n      // Send a report message to the uploader to hard-redirect them to the reported page.\n      // The uploader will broadcast a report message to all connections, which will hard-redirect all downloaders to the reported page.\n      const conn = peer.connect(uploaderPeerID, {\n        metadata: { type: 'report' },\n      })\n\n      // Set a timeout to redirect after 2 seconds even if connection doesn't open\n      const timeout = setTimeout(() => {\n        conn.close()\n        window.location.href = '/reported'\n      }, 2000)\n\n      conn.on('open', () => {\n        clearTimeout(timeout)\n        conn.close()\n        window.location.href = '/reported'\n      })\n    } catch (error) {\n      console.error('Failed to report violation', error)\n      setIsReporting(false)\n    }\n  }, [peer, uploaderPeerID])\n\n  return (\n    <>\n      <div className=\"flex justify-center\">\n        <button\n          onClick={() => setShowModal(true)}\n          className=\"text-sm text-red-600 dark:text-red-400 hover:underline transition-colors duration-200\"\n          aria-label=\"Report terms violation\"\n        >\n          Report suspicious pizza delivery\n        </button>\n      </div>\n\n      {showModal && (\n        <div\n          className=\"fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50\"\n          role=\"dialog\"\n          aria-modal=\"true\"\n          aria-labelledby=\"modal-title\"\n          onClick={() => setShowModal(false)}\n        >\n          <div\n            className=\"bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-700 rounded-lg p-8 max-w-md w-full shadow-lg\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            <h2\n              id=\"modal-title\"\n              className=\"text-xl font-bold mb-4 text-stone-900 dark:text-stone-50\"\n            >\n              Found a suspicious delivery?\n            </h2>\n\n            <div className=\"space-y-4 text-stone-700 dark:text-stone-300\">\n              <p>\n                Before reporting this delivery, please note our FilePizza terms:\n              </p>\n\n              <ul className=\"list-none space-y-3\">\n                <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n                  <span className=\"text-base\">✅</span>\n                  <span className=\"text-sm\">\n                    Only upload files you have the right to share\n                  </span>\n                </li>\n                <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n                  <span className=\"text-base\">🔒</span>\n                  <span className=\"text-sm\">\n                    Share download links only with known recipients\n                  </span>\n                </li>\n                <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n                  <span className=\"text-base\">⚠️</span>\n                  <span className=\"text-sm\">\n                    No illegal or harmful content allowed\n                  </span>\n                </li>\n              </ul>\n\n              <p>\n                If you've spotted a violation of these terms, click Report to\n                halt its delivery.\n              </p>\n            </div>\n\n            <div className=\"mt-6 flex justify-end space-x-4\">\n              <CancelButton onClick={() => setShowModal(false)} />\n              <button\n                disabled={isReporting}\n                onClick={handleReport}\n                className={`px-4 py-2 bg-linear-to-b from-red-500 to-red-600 text-white rounded-md border border-red-600 shadow-sm text-shadow disabled:opacity-50 disabled:cursor-not-allowed enabled:hover:from-red-500 enabled:hover:to-red-700 enabled:hover:shadow-md transition-all duration-200`}\n                aria-label=\"Confirm report\"\n              >\n                {isReporting ? 'Reporting...' : 'Report'}\n              </button>\n            </div>\n          </div>\n        </div>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/ReturnHome.tsx",
    "content": "import { Link } from 'next-view-transitions'\nimport { JSX } from 'react'\n\nexport default function ReturnHome(): JSX.Element {\n  return (\n    <div className=\"flex justify-center\">\n      <Link\n        href=\"/\"\n        className=\"text-stone-500 dark:text-stone-200 hover:underline\"\n      >\n        Serve up a fresh slice &raquo;\n      </Link>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Spinner.tsx",
    "content": "'use client'\n\nimport React, { JSX } from 'react'\nimport { useRotatingSpinner } from '../hooks/useRotatingSpinner'\n\nfunction Pizza({ isRotating }: { isRotating?: boolean }): JSX.Element {\n  return (\n    <svg\n      width=\"300\"\n      height=\"300\"\n      viewBox=\"0 0 577 576\"\n      fill=\"none\"\n      className={isRotating ? 'animate-spin-slow' : ''}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      role=\"img\"\n      aria-label={isRotating ? 'Rotating pizza' : 'Pizza'}\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M282.03 3C305.481 3 342.363 3.834 364.169 9.196C379.23 12.899 411.295 34.038 425.12 40.07C444.633 48.582 473.307 61.063 488.89 72.683C504.759 84.517 514.985 106.605 525.825 121.285C538.485 138.43 554.853 158.059 562.267 177.47C568.947 194.96 568.68 217.508 569.719 237.83C570.547 254.018 573.27 267.218 573.27 286.732C573.27 305.214 574.801 323.938 571.428 341.441C568.674 355.73 548.336 389.584 543.458 403.014C534.262 428.329 525.566 457.372 511.148 477.129C498.107 494.998 465.852 512.222 448.562 524.724C432.604 536.263 419.877 553.349 403.411 560.308C392.817 564.786 360.764 565.314 348.757 566.92C324.981 570.1 300.039 572.855 269.796 572.855C252.164 572.855 200.212 555.435 183.89 552.32C154.282 546.671 131.181 537.396 109.506 524.109C92.2349 513.522 73.1809 481.362 60.3559 466.431C45.9569 449.669 26.4779 423.234 18.0389 404.732C2.3219 370.274 7.3699 356.493 3.6579 297.645C2.4529 278.549 2.8439 255.235 5.3239 233.582C6.8889 219.925 9.2849 206.928 12.6369 196.067C22.1159 165.353 29.6899 141.087 49.6239 116.134C65.5429 96.206 95.6839 73.766 117.973 59.173C136.328 47.154 151.148 28.521 170.178 19.875C205.749 3.714 240.075 3 282.03 3Z\"\n        fill=\"#A9652E\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M282.229 10.8268C304.937 10.8268 340.651 11.6348 361.766 16.8268C376.351 20.4128 407.401 40.8818 420.788 46.7228C439.682 54.9658 467.449 67.0518 482.539 78.3038C497.905 89.7618 507.807 111.151 518.304 125.366C530.563 141.968 546.413 160.976 553.592 179.772C560.06 196.709 559.802 218.542 560.808 238.221C561.609 253.896 564.246 266.678 564.246 285.575C564.246 303.471 565.729 321.602 562.462 338.551C559.796 352.388 540.102 385.169 535.378 398.174C526.474 422.688 518.053 450.811 504.091 469.943C491.464 487.246 460.229 503.924 443.487 516.031C428.035 527.205 415.711 543.749 399.767 550.488C389.508 554.824 358.47 555.335 346.843 556.89C323.82 559.97 299.668 562.638 270.382 562.638C253.308 562.638 203.001 545.769 187.196 542.753C158.526 537.283 136.157 528.301 115.167 515.435C98.4439 505.184 79.9929 474.042 67.5739 459.584C53.6319 443.352 34.7689 417.754 26.5969 399.838C11.3779 366.471 16.2659 353.126 12.6709 296.142C11.5049 277.65 11.8829 255.075 14.2849 234.108C15.7999 220.883 18.1209 208.298 21.3659 197.78C30.5449 168.039 37.8789 144.541 57.1819 120.378C72.5969 101.082 101.784 79.3518 123.366 65.2208C141.141 53.5828 155.491 35.5398 173.919 27.1668C208.363 11.5178 241.603 10.8268 282.229 10.8268Z\"\n        fill=\"#C3783A\"\n      />\n      <path\n        d=\"M282.03 3C305.481 3 342.363 3.834 364.169 9.196C379.23 12.899 411.295 34.038 425.12 40.07C444.633 48.582 473.307 61.063 488.89 72.683C504.759 84.517 514.985 106.605 525.825 121.285C538.485 138.43 554.853 158.059 562.267 177.47C568.947 194.96 568.68 217.508 569.719 237.83C570.547 254.018 573.27 267.218 573.27 286.732C573.27 305.214 574.801 323.938 571.428 341.441C568.674 355.73 548.336 389.584 543.458 403.014C534.262 428.329 525.566 457.372 511.148 477.129C498.107 494.998 465.852 512.222 448.562 524.724C432.604 536.263 419.877 553.349 403.411 560.308C392.817 564.786 360.764 565.314 348.757 566.92C324.981 570.1 300.039 572.855 269.796 572.855C252.164 572.855 200.212 555.435 183.89 552.32C154.282 546.671 131.181 537.396 109.506 524.109C92.2349 513.522 73.1809 481.362 60.3559 466.431C45.9569 449.669 26.4779 423.234 18.0389 404.732C2.3219 370.274 7.3699 356.493 3.6579 297.645C2.4529 278.549 2.8439 255.235 5.3239 233.582C6.8889 219.925 9.2849 206.928 12.6369 196.067C22.1159 165.353 29.6899 141.087 49.6239 116.134C65.5429 96.206 95.6839 73.766 117.973 59.173C136.328 47.154 151.148 28.521 170.178 19.875C205.749 3.714 240.075 3 282.03 3Z\"\n        stroke=\"#521E11\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M288.827 27.9578C322.962 27.9578 343.427 30.3268 369.094 42.1828C383.616 48.8908 404.212 62.3178 418.983 70.9598C434.772 80.1978 463.535 85.2468 476.196 97.4908C488.29 109.188 493.836 123.556 503.41 137.451C517.367 157.706 535.196 169.274 541.456 196.128C543.311 204.085 541.92 225.168 542.898 239.887C544.246 260.168 547.602 274.995 547.602 286.732C547.602 301.57 547.444 311.961 543.812 325.591C538.352 346.078 527.707 369.271 519.219 389.062C508.6 413.822 500.922 440.868 483.459 460.315C469.9 475.416 452.058 483.657 435.682 495.821C418.507 508.578 403.409 526.345 383.46 534.196C370.534 539.284 357.051 537.558 343.209 539.098C325.657 541.052 307.496 545.507 288.827 545.507C260.205 545.507 228.873 532.544 200.966 523.887C177.285 516.54 156.299 514.448 139.127 503.351C113.983 487.102 106.472 470.413 88.5909 445.642C73.8849 425.27 54.3409 407.02 47.0689 380.547C40.8269 357.829 38.6499 316.505 38.6499 291.405C38.6499 262.136 34.9219 229.322 43.8959 203.076C55.1649 170.115 63.2489 143.39 86.8969 119.311C100.909 105.044 127.85 90.9798 143.827 79.0738C161.812 65.6698 174.247 48.0068 196.099 40.2028C212.441 34.3678 227.463 33.8128 246.799 32.1528C259.618 31.0518 276.67 27.9578 288.827 27.9578Z\"\n        fill=\"#B52720\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M288.946 35.8318C322.042 35.8318 341.885 38.1288 366.77 49.6238C380.851 56.1278 400.82 69.1458 415.141 77.5248C430.451 86.4818 458.338 91.3778 470.614 103.25C482.34 114.59 487.717 128.521 497 141.994C510.532 161.633 527.819 172.848 533.888 198.885C535.687 206.6 534.338 227.041 535.286 241.313C536.593 260.976 539.847 275.352 539.847 286.732C539.847 301.118 539.694 311.193 536.172 324.408C530.879 344.272 520.557 366.76 512.327 385.948C502.031 409.955 494.587 436.178 477.656 455.033C464.509 469.675 447.211 477.665 431.332 489.459C414.68 501.828 400.041 519.054 380.7 526.666C368.167 531.599 355.094 529.925 341.673 531.419C324.655 533.313 307.047 537.633 288.946 537.633C261.195 537.633 230.817 525.065 203.759 516.67C180.798 509.547 160.451 507.519 143.802 496.76C119.423 481.005 112.14 464.824 94.8029 440.806C80.5449 421.055 61.5949 403.36 54.5439 377.693C48.4929 355.665 46.3809 315.599 46.3809 291.263C46.3809 262.884 42.7679 231.069 51.4679 205.621C62.3939 173.663 70.2319 147.752 93.1609 124.406C106.746 110.572 132.867 96.9368 148.358 85.3918C165.796 72.3968 177.853 55.2708 199.04 47.7048C214.885 42.0468 229.45 41.5088 248.197 39.8998C260.626 38.8318 277.159 35.8318 288.946 35.8318Z\"\n        fill=\"#E03C32\"\n      />\n      <path\n        d=\"M288.827 27.9578C322.962 27.9578 343.427 30.3268 369.094 42.1828C383.616 48.8908 404.212 62.3178 418.983 70.9598C434.772 80.1978 463.535 85.2468 476.196 97.4908C488.29 109.188 493.836 123.556 503.41 137.451C517.367 157.706 535.196 169.274 541.456 196.128C543.311 204.085 541.92 225.168 542.898 239.887C544.246 260.168 547.602 274.995 547.602 286.732C547.602 301.57 547.444 311.961 543.812 325.591C538.352 346.078 527.707 369.271 519.219 389.062C508.6 413.822 500.922 440.868 483.459 460.315C469.9 475.416 452.058 483.657 435.682 495.821C418.507 508.578 403.409 526.345 383.46 534.196C370.534 539.284 357.051 537.558 343.209 539.098C325.657 541.052 307.496 545.507 288.827 545.507C260.205 545.507 228.873 532.544 200.966 523.887C177.285 516.54 156.299 514.448 139.127 503.351C113.983 487.102 106.472 470.413 88.5909 445.642C73.8849 425.27 54.3409 407.02 47.0689 380.547C40.8269 357.829 38.6499 316.505 38.6499 291.405C38.6499 262.136 34.9219 229.322 43.8959 203.076C55.1649 170.115 63.2489 143.39 86.8969 119.311C100.909 105.044 127.85 90.9798 143.827 79.0738C161.812 65.6698 174.247 48.0068 196.099 40.2028C212.441 34.3678 227.463 33.8128 246.799 32.1528C259.618 31.0518 276.67 27.9578 288.827 27.9578Z\"\n        stroke=\"#521E11\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M288.827 47.5979C328.62 47.5979 358.795 55.9589 392.033 75.4339C438.72 102.789 424.814 100.89 453.232 127.777C476.652 149.935 494.302 155.385 503.41 173.492C516.145 198.809 522.449 258.95 525.635 286.732C527.837 305.938 510.317 338.851 503.41 353.367C485.868 390.233 489.753 419.213 473.846 439.761C453.911 465.512 415.108 479.154 403.411 491.436C372.26 524.145 362.7 520.159 300.403 515.358C281.069 513.868 244.487 518.845 228.98 511.2C191.2 492.571 186.156 484.616 171.894 476.258C150.279 463.591 124.1 451.699 107.704 434.742C80.6339 406.746 57.4639 365.527 60.6499 319.407C63.3479 280.351 69.4449 213.429 76.8329 187.094C81.9619 168.814 97.0939 140.021 116.199 123.684C124.097 116.931 155.922 95.0779 164.746 89.3089C174.983 82.6169 210.847 62.8899 222 58.2989C242.886 49.6999 265.646 47.5979 288.827 47.5979Z\"\n        fill=\"#EBA900\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M288.998 57.0408C327.195 57.0408 356.16 65.0668 388.065 83.7598C432.881 110.018 419.532 108.196 446.811 134.004C469.291 155.274 486.234 160.505 494.977 177.886C507.201 202.188 513.252 259.918 516.31 286.586C518.424 305.021 501.607 336.615 494.977 350.548C478.138 385.936 481.867 413.755 466.598 433.478C447.462 458.197 410.215 471.292 398.988 483.081C369.085 514.479 359.909 510.652 300.11 506.044C281.551 504.613 246.436 509.392 231.551 502.052C195.285 484.171 190.444 476.534 176.754 468.512C156.006 456.353 130.876 444.938 115.137 428.66C89.1529 401.787 66.9119 362.221 69.9699 317.95C72.5609 280.46 78.4129 216.222 85.5049 190.943C90.4279 173.396 104.953 145.758 123.292 130.076C130.873 123.593 161.422 102.617 169.893 97.0788C179.719 90.6558 214.145 71.7198 224.851 67.3118C244.899 59.0578 266.746 57.0408 288.998 57.0408Z\"\n        fill=\"#FCC534\"\n      />\n      <path\n        d=\"M288.827 47.5979C328.62 47.5979 358.795 55.9589 392.033 75.4339C438.72 102.789 424.814 100.89 453.232 127.777C476.652 149.935 494.302 155.385 503.41 173.492C516.145 198.809 522.449 258.95 525.635 286.732C527.837 305.938 510.317 338.851 503.41 353.367C485.868 390.233 489.753 419.213 473.846 439.761C453.911 465.512 415.108 479.154 403.411 491.436C372.26 524.145 362.7 520.159 300.403 515.358C281.069 513.868 244.487 518.845 228.98 511.2C191.2 492.571 186.156 484.616 171.894 476.258C150.279 463.591 124.1 451.699 107.704 434.742C80.6339 406.746 57.4639 365.527 60.6499 319.407C63.3479 280.351 69.4449 213.429 76.8329 187.094C81.9619 168.814 97.0939 140.021 116.199 123.684C124.097 116.931 155.922 95.0779 164.746 89.3089C174.983 82.6169 210.847 62.8899 222 58.2989C242.886 49.6999 265.646 47.5979 288.827 47.5979Z\"\n        stroke=\"#521E11\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M151.149 221.694C132.103 221.694 116.663 206.254 116.663 187.208C116.663 168.162 132.103 152.722 151.149 152.722C170.195 152.722 185.635 168.162 185.635 187.208C185.635 206.254 170.195 221.694 151.149 221.694Z\"\n        fill=\"#D53B1C\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M129.892 214.356C125.58 208.475 123.026 201.182 123.026 193.286C123.026 173.812 138.56 158.002 157.695 158.002C164.428 158.002 170.716 159.96 176.037 163.346C181.981 169.543 185.635 177.952 185.635 187.208C185.635 206.241 170.182 221.694 151.149 221.694C143.132 221.694 135.751 218.952 129.892 214.356Z\"\n        fill=\"#E84827\"\n      />\n      <path\n        d=\"M151.149 221.694C132.103 221.694 116.663 206.254 116.663 187.208C116.663 168.162 132.103 152.722 151.149 152.722C170.195 152.722 185.635 168.162 185.635 187.208C185.635 206.254 170.195 221.694 151.149 221.694Z\"\n        stroke=\"#8D3A2B\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M147.274 173.163C144.587 173.163 142.409 170.985 142.409 168.299C142.409 165.612 144.587 163.435 147.274 163.435C149.961 163.435 152.139 165.612 152.139 168.299C152.139 170.985 149.961 173.163 147.274 173.163Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M162.685 209.193C158.559 209.193 155.214 205.848 155.214 201.722C155.214 197.596 158.559 194.251 162.685 194.251C166.811 194.251 170.156 197.596 170.156 201.722C170.156 205.848 166.811 209.193 162.685 209.193Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M167.549 180.431C163.535 180.431 160.282 177.177 160.282 173.164C160.282 169.15 163.535 165.897 167.549 165.897C171.562 165.897 174.816 169.15 174.816 173.164C174.816 177.177 171.562 180.431 167.549 180.431Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M130.322 183.492C128.31 183.492 126.679 181.861 126.679 179.849C126.679 177.837 128.31 176.206 130.322 176.206C132.334 176.206 133.965 177.837 133.965 179.849C133.965 181.861 132.334 183.492 130.322 183.492Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M148.934 213.169C147.597 213.169 146.513 212.085 146.513 210.748C146.513 209.411 147.597 208.327 148.934 208.327C150.271 208.327 151.355 209.411 151.355 210.748C151.355 212.085 150.271 213.169 148.934 213.169Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M153.775 179.541C152.438 179.541 151.355 178.457 151.355 177.121C151.355 175.784 152.438 174.701 153.775 174.701C155.111 174.701 156.195 175.784 156.195 177.121C156.195 178.457 155.111 179.541 153.775 179.541Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M139.864 208.328C134.854 208.328 130.793 204.267 130.793 199.257C130.793 194.247 134.854 190.186 139.864 190.186C144.874 190.186 148.935 194.247 148.935 199.257C148.935 204.267 144.874 208.328 139.864 208.328Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M156.917 189.444C155.682 189.444 154.681 188.443 154.681 187.208C154.681 185.973 155.682 184.972 156.917 184.972C158.152 184.972 159.153 185.973 159.153 187.208C159.153 188.443 158.152 189.444 156.917 189.444Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M175.918 184.972C174.683 184.972 173.682 183.971 173.682 182.736C173.682 181.501 174.683 180.5 175.918 180.5C177.153 180.5 178.154 181.501 178.154 182.736C178.154 183.971 177.153 184.972 175.918 184.972Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M141.661 180.501C140.426 180.501 139.425 179.5 139.425 178.265C139.425 177.03 140.426 176.029 141.661 176.029C142.896 176.029 143.897 177.03 143.897 178.265C143.897 179.5 142.896 180.501 141.661 180.501Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M127.838 191.679C126.603 191.679 125.602 190.678 125.602 189.443C125.602 188.208 126.603 187.207 127.838 187.207C129.073 187.207 130.074 188.208 130.074 189.443C130.074 190.678 129.073 191.679 127.838 191.679Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M173.682 196.151C172.447 196.151 171.446 195.15 171.446 193.915C171.446 192.68 172.447 191.679 173.682 191.679C174.917 191.679 175.918 192.68 175.918 193.915C175.918 195.15 174.917 196.151 173.682 196.151Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M367.24 302.877C350.879 302.877 337.616 289.614 337.616 273.253C337.616 256.892 350.879 243.629 367.24 243.629C383.601 243.629 396.864 256.892 396.864 273.253C396.864 289.614 383.601 302.877 367.24 302.877Z\"\n        fill=\"#D53B1C\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M348.982 296.575C345.277 291.523 343.083 285.257 343.083 278.475C343.083 261.747 356.427 248.166 372.863 248.166C378.648 248.166 384.05 249.848 388.621 252.758C393.726 258.081 396.864 265.304 396.864 273.253C396.864 289.603 383.59 302.877 367.241 302.877C360.355 302.877 354.015 300.522 348.982 296.575Z\"\n        fill=\"#E84827\"\n      />\n      <path\n        d=\"M367.24 302.877C350.879 302.877 337.616 289.614 337.616 273.253C337.616 256.892 350.879 243.629 367.24 243.629C383.601 243.629 396.864 256.892 396.864 273.253C396.864 289.614 383.601 302.877 367.24 302.877Z\"\n        stroke=\"#8D3A2B\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M363.912 261.189C361.604 261.189 359.733 259.318 359.733 257.01C359.733 254.702 361.604 252.831 363.912 252.831C366.22 252.831 368.091 254.702 368.091 257.01C368.091 259.318 366.22 261.189 363.912 261.189Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M377.149 292.137C373.604 292.137 370.731 289.264 370.731 285.72C370.731 282.176 373.604 279.303 377.149 279.303C380.693 279.303 383.567 282.176 383.567 285.72C383.567 289.264 380.693 292.137 377.149 292.137Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M381.328 267.432C377.88 267.432 375.085 264.637 375.085 261.189C375.085 257.741 377.88 254.946 381.328 254.946C384.776 254.946 387.571 257.741 387.571 261.189C387.571 264.637 384.776 267.432 381.328 267.432Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M349.349 270.061C347.621 270.061 346.22 268.66 346.22 266.932C346.22 265.204 347.621 263.803 349.349 263.803C351.077 263.803 352.478 265.204 352.478 266.932C352.478 268.66 351.077 270.061 349.349 270.061Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M365.338 295.553C364.19 295.553 363.259 294.622 363.259 293.474C363.259 292.326 364.19 291.395 365.338 291.395C366.486 291.395 367.417 292.326 367.417 293.474C367.417 294.622 366.486 295.553 365.338 295.553Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M369.496 266.667C368.348 266.667 367.417 265.736 367.417 264.588C367.417 263.44 368.348 262.509 369.496 262.509C370.644 262.509 371.575 263.44 371.575 264.588C371.575 265.736 370.644 266.667 369.496 266.667Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M357.546 291.395C353.243 291.395 349.755 287.907 349.755 283.604C349.755 279.301 353.243 275.813 357.546 275.813C361.849 275.813 365.337 279.301 365.337 283.604C365.337 287.907 361.849 291.395 357.546 291.395Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M372.195 275.174C371.134 275.174 370.274 274.314 370.274 273.253C370.274 272.192 371.134 271.332 372.195 271.332C373.256 271.332 374.116 272.192 374.116 273.253C374.116 274.314 373.256 275.174 372.195 275.174Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M388.518 271.332C387.457 271.332 386.598 270.472 386.598 269.412C386.598 268.351 387.457 267.492 388.518 267.492C389.578 267.492 390.438 268.351 390.438 269.412C390.438 270.472 389.578 271.332 388.518 271.332Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M359.09 267.492C358.03 267.492 357.17 266.632 357.17 265.572C357.17 264.511 358.03 263.652 359.09 263.652C360.15 263.652 361.01 264.511 361.01 265.572C361.01 266.632 360.15 267.492 359.09 267.492Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M347.216 277.095C346.155 277.095 345.295 276.235 345.295 275.174C345.295 274.113 346.155 273.253 347.216 273.253C348.277 273.253 349.137 274.113 349.137 275.174C349.137 276.235 348.277 277.095 347.216 277.095Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M386.596 280.936C385.535 280.936 384.675 280.076 384.675 279.015C384.675 277.954 385.535 277.094 386.596 277.094C387.657 277.094 388.517 277.954 388.517 279.015C388.517 280.076 387.657 280.936 386.596 280.936Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M355.095 150.637C335.321 150.637 319.291 134.607 319.291 114.833C319.291 95.0588 335.321 79.0288 355.095 79.0288C374.869 79.0288 390.899 95.0588 390.899 114.833C390.899 134.607 374.869 150.637 355.095 150.637Z\"\n        fill=\"#D53B1C\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M333.026 143.019C328.549 136.912 325.898 129.34 325.898 121.143C325.898 100.925 342.026 84.5107 361.891 84.5107C368.882 84.5107 375.41 86.5437 380.935 90.0587C387.105 96.4927 390.899 105.223 390.899 114.832C390.899 134.593 374.856 150.636 355.096 150.636C346.773 150.636 339.109 147.79 333.026 143.019Z\"\n        fill=\"#E84827\"\n      />\n      <path\n        d=\"M355.095 150.637C335.321 150.637 319.291 134.607 319.291 114.833C319.291 95.0588 335.321 79.0288 355.095 79.0288C374.869 79.0288 390.899 95.0588 390.899 114.833C390.899 134.607 374.869 150.637 355.095 150.637Z\"\n        stroke=\"#8D3A2B\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M351.072 100.251C348.283 100.251 346.022 97.9898 346.022 95.2007C346.022 92.4117 348.283 90.1508 351.072 90.1508C353.861 90.1508 356.122 92.4117 356.122 95.2007C356.122 97.9898 353.861 100.251 351.072 100.251Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M367.072 137.656C362.788 137.656 359.316 134.183 359.316 129.9C359.316 125.616 362.788 122.144 367.072 122.144C371.355 122.144 374.828 125.616 374.828 129.9C374.828 134.183 371.355 137.656 367.072 137.656Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M372.122 107.797C367.954 107.797 364.576 104.418 364.576 100.251C364.576 96.0833 367.954 92.7048 372.122 92.7048C376.289 92.7048 379.668 96.0833 379.668 100.251C379.668 104.418 376.289 107.797 372.122 107.797Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M333.472 110.974C331.383 110.974 329.69 109.281 329.69 107.192C329.69 105.103 331.383 103.41 333.472 103.41C335.561 103.41 337.254 105.103 337.254 107.192C337.254 109.281 335.561 110.974 333.472 110.974Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M352.796 141.784C351.408 141.784 350.283 140.659 350.283 139.271C350.283 137.883 351.408 136.758 352.796 136.758C354.184 136.758 355.309 137.883 355.309 139.271C355.309 140.659 354.184 141.784 352.796 141.784Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M357.822 106.872C356.434 106.872 355.309 105.747 355.309 104.359C355.309 102.971 356.434 101.846 357.822 101.846C359.21 101.846 360.335 102.971 360.335 104.359C360.335 105.747 359.21 106.872 357.822 106.872Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M343.379 136.759C338.178 136.759 333.962 132.543 333.962 127.342C333.962 122.141 338.178 117.925 343.379 117.925C348.58 117.925 352.796 122.141 352.796 127.342C352.796 132.543 348.58 136.759 343.379 136.759Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M361.084 117.154C359.802 117.154 358.763 116.115 358.763 114.833C358.763 113.551 359.802 112.512 361.084 112.512C362.366 112.512 363.405 113.551 363.405 114.833C363.405 116.115 362.366 117.154 361.084 117.154Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M380.811 112.511C379.529 112.511 378.49 111.472 378.49 110.19C378.49 108.908 379.529 107.869 380.811 107.869C382.093 107.869 383.132 108.908 383.132 110.19C383.132 111.472 382.093 112.511 380.811 112.511Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M345.245 107.869C343.963 107.869 342.924 106.83 342.924 105.548C342.924 104.266 343.963 103.227 345.245 103.227C346.527 103.227 347.566 104.266 347.566 105.548C347.566 106.83 346.527 107.869 345.245 107.869Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M330.893 119.475C329.611 119.475 328.572 118.436 328.572 117.154C328.572 115.872 329.611 114.833 330.893 114.833C332.175 114.833 333.214 115.872 333.214 117.154C333.214 118.436 332.175 119.475 330.893 119.475Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M378.49 124.117C377.208 124.117 376.169 123.078 376.169 121.796C376.169 120.514 377.208 119.475 378.49 119.475C379.772 119.475 380.811 120.514 380.811 121.796C380.811 123.078 379.772 124.117 378.49 124.117Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M120.169 323.588C102.11 323.588 87.4699 308.948 87.4699 290.889C87.4699 272.83 102.11 258.19 120.169 258.19C138.228 258.19 152.868 272.83 152.868 290.889C152.868 308.948 138.228 323.588 120.169 323.588Z\"\n        fill=\"#D53B1C\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M100.013 316.631C95.9239 311.054 93.5029 304.139 93.5029 296.652C93.5029 278.188 108.232 263.197 126.375 263.197C132.761 263.197 138.724 265.054 143.77 268.266C149.404 274.142 152.868 282.114 152.868 290.889C152.868 308.936 138.216 323.588 120.169 323.588C112.567 323.588 105.568 320.989 100.013 316.631Z\"\n        fill=\"#E84827\"\n      />\n      <path\n        d=\"M120.169 323.588C102.11 323.588 87.4699 308.948 87.4699 290.889C87.4699 272.83 102.11 258.19 120.169 258.19C138.228 258.19 152.868 272.83 152.868 290.889C152.868 308.948 138.228 323.588 120.169 323.588Z\"\n        stroke=\"#8D3A2B\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M116.495 277.572C113.947 277.572 111.882 275.507 111.882 272.959C111.882 270.411 113.947 268.346 116.495 268.346C119.043 268.346 121.108 270.411 121.108 272.959C121.108 275.507 119.043 277.572 116.495 277.572Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M131.107 311.734C127.195 311.734 124.023 308.562 124.023 304.65C124.023 300.737 127.195 297.566 131.107 297.566C135.019 297.566 138.191 300.737 138.191 304.65C138.191 308.562 135.019 311.734 131.107 311.734Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M135.719 284.463C131.913 284.463 128.828 281.378 128.828 277.572C128.828 273.766 131.913 270.681 135.719 270.681C139.525 270.681 142.61 273.766 142.61 277.572C142.61 281.378 139.525 284.463 135.719 284.463Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M100.42 287.365C98.5123 287.365 96.9659 285.818 96.9659 283.911C96.9659 282.003 98.5123 280.457 100.42 280.457C102.327 280.457 103.874 282.003 103.874 283.911C103.874 285.818 102.327 287.365 100.42 287.365Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M118.069 315.504C116.801 315.504 115.774 314.476 115.774 313.209C115.774 311.941 116.801 310.914 118.069 310.914C119.336 310.914 120.364 311.941 120.364 313.209C120.364 314.476 119.336 315.504 118.069 315.504Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M122.659 283.619C121.391 283.619 120.364 282.591 120.364 281.324C120.364 280.056 121.391 279.029 122.659 279.029C123.926 279.029 124.954 280.056 124.954 281.324C124.954 282.591 123.926 283.619 122.659 283.619Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M109.468 310.914C104.718 310.914 100.867 307.063 100.867 302.313C100.867 297.563 104.718 293.712 109.468 293.712C114.218 293.712 118.069 297.563 118.069 302.313C118.069 307.063 114.218 310.914 109.468 310.914Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M125.637 293.009C124.466 293.009 123.517 292.06 123.517 290.889C123.517 289.718 124.466 288.769 125.637 288.769C126.808 288.769 127.757 289.718 127.757 290.889C127.757 292.06 126.808 293.009 125.637 293.009Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M143.654 288.769C142.483 288.769 141.534 287.82 141.534 286.649C141.534 285.478 142.483 284.529 143.654 284.529C144.825 284.529 145.774 285.478 145.774 286.649C145.774 287.82 144.825 288.769 143.654 288.769Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M111.172 284.529C110.001 284.529 109.052 283.58 109.052 282.409C109.052 281.238 110.001 280.289 111.172 280.289C112.343 280.289 113.292 281.238 113.292 282.409C113.292 283.58 112.343 284.529 111.172 284.529Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M98.0649 295.129C96.8941 295.129 95.9449 294.18 95.9449 293.009C95.9449 291.838 96.8941 290.889 98.0649 290.889C99.2358 290.889 100.185 291.838 100.185 293.009C100.185 294.18 99.2358 295.129 98.0649 295.129Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M141.534 299.367C140.363 299.367 139.414 298.418 139.414 297.248C139.414 296.078 140.363 295.129 141.534 295.129C142.705 295.129 143.654 296.078 143.654 297.248C143.654 298.418 142.705 299.367 141.534 299.367Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M239.924 359.39C220.15 359.39 204.121 343.36 204.121 323.587C204.121 303.813 220.15 287.784 239.924 287.784C259.697 287.784 275.727 303.813 275.727 323.587C275.727 343.36 259.697 359.39 239.924 359.39Z\"\n        fill=\"#D53B1C\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M217.854 351.774C213.377 345.667 210.726 338.095 210.726 329.898C210.726 309.68 226.854 293.266 246.719 293.266C253.71 293.266 260.239 295.299 265.764 298.816C271.934 305.249 275.727 313.979 275.727 323.588C275.727 343.348 259.684 359.391 239.924 359.391C231.601 359.391 223.937 356.545 217.854 351.774Z\"\n        fill=\"#E84827\"\n      />\n      <path\n        d=\"M239.924 359.39C220.15 359.39 204.121 343.36 204.121 323.587C204.121 303.813 220.15 287.784 239.924 287.784C259.697 287.784 275.727 303.813 275.727 323.587C275.727 343.36 259.697 359.39 239.924 359.39Z\"\n        stroke=\"#8D3A2B\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M235.9 309.005C233.111 309.005 230.85 306.744 230.85 303.955C230.85 301.166 233.111 298.905 235.9 298.905C238.689 298.905 240.95 301.166 240.95 303.955C240.95 306.744 238.689 309.005 235.9 309.005Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M251.9 346.412C247.616 346.412 244.144 342.939 244.144 338.656C244.144 334.372 247.616 330.9 251.9 330.9C256.183 330.9 259.656 334.372 259.656 338.656C259.656 342.939 256.183 346.412 251.9 346.412Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M256.95 316.552C252.783 316.552 249.405 313.174 249.405 309.007C249.405 304.84 252.783 301.462 256.95 301.462C261.117 301.462 264.495 304.84 264.495 309.007C264.495 313.174 261.117 316.552 256.95 316.552Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M218.299 319.73C216.21 319.73 214.517 318.037 214.517 315.948C214.517 313.859 216.21 312.166 218.299 312.166C220.388 312.166 222.081 313.859 222.081 315.948C222.081 318.037 220.388 319.73 218.299 319.73Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M237.624 350.54C236.236 350.54 235.111 349.415 235.111 348.027C235.111 346.639 236.236 345.514 237.624 345.514C239.012 345.514 240.137 346.639 240.137 348.027C240.137 349.415 239.012 350.54 237.624 350.54Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M242.65 315.628C241.262 315.628 240.137 314.503 240.137 313.115C240.137 311.727 241.262 310.602 242.65 310.602C244.038 310.602 245.163 311.727 245.163 313.115C245.163 314.503 244.038 315.628 242.65 315.628Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M228.207 345.514C223.006 345.514 218.79 341.298 218.79 336.097C218.79 330.896 223.006 326.68 228.207 326.68C233.408 326.68 237.624 330.896 237.624 336.097C237.624 341.298 233.408 345.514 228.207 345.514Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M245.912 325.909C244.63 325.909 243.591 324.87 243.591 323.588C243.591 322.306 244.63 321.267 245.912 321.267C247.194 321.267 248.233 322.306 248.233 323.588C248.233 324.87 247.194 325.909 245.912 325.909Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M265.639 321.267C264.357 321.267 263.318 320.227 263.318 318.945C263.318 317.662 264.357 316.623 265.639 316.623C266.921 316.623 267.96 317.662 267.96 318.945C267.96 320.227 266.921 321.267 265.639 321.267Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M230.073 316.624C228.791 316.624 227.752 315.585 227.752 314.303C227.752 313.021 228.791 311.982 230.073 311.982C231.355 311.982 232.394 313.021 232.394 314.303C232.394 315.585 231.355 316.624 230.073 316.624Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M215.721 328.23C214.439 328.23 213.4 327.191 213.4 325.909C213.4 324.627 214.439 323.588 215.721 323.588C217.003 323.588 218.042 324.627 218.042 325.909C218.042 327.191 217.003 328.23 215.721 328.23Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M263.318 332.872C262.036 332.872 260.997 331.833 260.997 330.551C260.997 329.269 262.036 328.23 263.318 328.23C264.6 328.23 265.639 329.269 265.639 330.551C265.639 331.833 264.6 332.872 263.318 332.872Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M422.589 391.401C402.815 391.401 386.785 375.371 386.785 355.597C386.785 335.823 402.815 319.793 422.589 319.793C442.363 319.793 458.393 335.823 458.393 355.597C458.393 375.371 442.363 391.401 422.589 391.401Z\"\n        fill=\"#D53B1C\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M400.518 383.783C396.042 377.676 393.391 370.105 393.391 361.909C393.391 341.69 409.519 325.276 429.384 325.276C436.375 325.276 442.904 327.309 448.429 330.826C454.599 337.259 458.392 345.989 458.392 355.598C458.392 375.358 442.349 391.401 422.589 391.401C414.265 391.401 406.601 388.554 400.518 383.783Z\"\n        fill=\"#E84827\"\n      />\n      <path\n        d=\"M422.589 391.401C402.815 391.401 386.785 375.371 386.785 355.597C386.785 335.823 402.815 319.793 422.589 319.793C442.363 319.793 458.393 335.823 458.393 355.597C458.393 375.371 442.363 391.401 422.589 391.401Z\"\n        stroke=\"#8D3A2B\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M418.565 341.016C415.776 341.016 413.515 338.755 413.515 335.966C413.515 333.177 415.776 330.916 418.565 330.916C421.354 330.916 423.615 333.177 423.615 335.966C423.615 338.755 421.354 341.016 418.565 341.016Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M434.565 378.422C430.281 378.422 426.809 374.949 426.809 370.666C426.809 366.382 430.281 362.91 434.565 362.91C438.848 362.91 442.321 366.382 442.321 370.666C442.321 374.949 438.848 378.422 434.565 378.422Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M439.615 348.563C435.447 348.563 432.069 345.184 432.069 341.017C432.069 336.849 435.447 333.471 439.615 333.471C443.782 333.471 447.161 336.849 447.161 341.017C447.161 345.184 443.782 348.563 439.615 348.563Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M400.965 351.74C398.876 351.74 397.183 350.047 397.183 347.958C397.183 345.869 398.876 344.176 400.965 344.176C403.054 344.176 404.747 345.869 404.747 347.958C404.747 350.047 403.054 351.74 400.965 351.74Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M420.29 382.55C418.902 382.55 417.777 381.425 417.777 380.037C417.777 378.649 418.902 377.524 420.29 377.524C421.678 377.524 422.803 378.649 422.803 380.037C422.803 381.425 421.678 382.55 420.29 382.55Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M425.315 347.638C423.927 347.638 422.802 346.513 422.802 345.125C422.802 343.737 423.927 342.612 425.315 342.612C426.703 342.612 427.828 343.737 427.828 345.125C427.828 346.513 426.703 347.638 425.315 347.638Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M410.873 377.524C405.672 377.524 401.456 373.308 401.456 368.107C401.456 362.906 405.672 358.69 410.873 358.69C416.074 358.69 420.29 362.906 420.29 368.107C420.29 373.308 416.074 377.524 410.873 377.524Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M428.577 357.919C427.295 357.919 426.256 356.88 426.256 355.598C426.256 354.316 427.295 353.277 428.577 353.277C429.859 353.277 430.898 354.316 430.898 355.598C430.898 356.88 429.859 357.919 428.577 357.919Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M448.304 353.278C447.022 353.278 445.983 352.238 445.983 350.956C445.983 349.673 447.022 348.634 448.304 348.634C449.586 348.634 450.625 349.673 450.625 350.956C450.625 352.238 449.586 353.278 448.304 353.278Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M412.738 348.634C411.456 348.634 410.417 347.595 410.417 346.313C410.417 345.031 411.456 343.992 412.738 343.992C414.02 343.992 415.059 345.031 415.059 346.313C415.059 347.595 414.02 348.634 412.738 348.634Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M398.387 360.24C397.105 360.24 396.066 359.201 396.066 357.919C396.066 356.637 397.105 355.598 398.387 355.598C399.669 355.598 400.708 356.637 400.708 357.919C400.708 359.201 399.669 360.24 398.387 360.24Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M445.983 364.882C444.701 364.882 443.662 363.843 443.662 362.561C443.662 361.279 444.701 360.24 445.983 360.24C447.265 360.24 448.304 361.279 448.304 362.561C448.304 363.843 447.265 364.882 445.983 364.882Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M472.815 307.355C460.665 307.355 450.815 297.505 450.815 285.355C450.815 273.205 460.665 263.355 472.815 263.355C484.965 263.355 494.815 273.205 494.815 285.355C494.815 297.505 484.965 307.355 472.815 307.355Z\"\n        fill=\"#D53B1C\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M459.254 302.672C456.504 298.921 454.875 294.268 454.875 289.232C454.875 276.809 464.785 266.723 476.991 266.723C481.287 266.723 485.298 267.972 488.693 270.133C492.484 274.086 494.815 279.45 494.815 285.354C494.815 297.496 484.958 307.354 472.816 307.354C467.701 307.354 462.992 305.605 459.254 302.672Z\"\n        fill=\"#E84827\"\n      />\n      <path\n        d=\"M472.815 307.355C460.665 307.355 450.815 297.505 450.815 285.355C450.815 273.205 460.665 263.355 472.815 263.355C484.965 263.355 494.815 273.205 494.815 285.355C494.815 297.505 484.965 307.355 472.815 307.355Z\"\n        stroke=\"#8D3A2B\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M470.343 276.395C468.629 276.395 467.24 275.006 467.24 273.292C467.24 271.578 468.629 270.189 470.343 270.189C472.057 270.189 473.446 271.578 473.446 273.292C473.446 275.006 472.057 276.395 470.343 276.395Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M480.175 299.379C477.543 299.379 475.409 297.245 475.409 294.613C475.409 291.981 477.543 289.847 480.175 289.847C482.807 289.847 484.941 291.981 484.941 294.613C484.941 297.245 482.807 299.379 480.175 299.379Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M483.278 281.031C480.718 281.031 478.642 278.955 478.642 276.395C478.642 273.834 480.718 271.759 483.278 271.759C485.838 271.759 487.914 273.834 487.914 276.395C487.914 278.955 485.838 281.031 483.278 281.031Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M459.528 282.984C458.244 282.984 457.204 281.943 457.204 280.66C457.204 279.376 458.244 278.336 459.528 278.336C460.811 278.336 461.852 279.376 461.852 280.66C461.852 281.943 460.811 282.984 459.528 282.984Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M471.403 301.915C470.55 301.915 469.859 301.224 469.859 300.371C469.859 299.518 470.55 298.827 471.403 298.827C472.256 298.827 472.947 299.518 472.947 300.371C472.947 301.224 472.256 301.915 471.403 301.915Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M474.491 280.464C473.638 280.464 472.947 279.772 472.947 278.92C472.947 278.067 473.638 277.376 474.491 277.376C475.344 277.376 476.035 278.067 476.035 278.92C476.035 279.772 475.344 280.464 474.491 280.464Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M465.617 298.827C462.421 298.827 459.831 296.236 459.831 293.041C459.831 289.845 462.421 287.255 465.617 287.255C468.812 287.255 471.403 289.845 471.403 293.041C471.403 296.236 468.812 298.827 465.617 298.827Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M476.495 286.781C475.707 286.781 475.069 286.142 475.069 285.354C475.069 284.566 475.707 283.927 476.495 283.927C477.282 283.927 477.921 284.566 477.921 285.354C477.921 286.142 477.282 286.781 476.495 286.781Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M488.617 283.928C487.829 283.928 487.19 283.289 487.19 282.502C487.19 281.714 487.829 281.076 488.617 281.076C489.405 281.076 490.044 281.714 490.044 282.502C490.044 283.289 489.405 283.928 488.617 283.928Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M466.763 281.077C465.975 281.077 465.337 280.438 465.337 279.65C465.337 278.862 465.975 278.223 466.763 278.223C467.55 278.223 468.189 278.862 468.189 279.65C468.189 280.438 467.55 281.077 466.763 281.077Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M457.945 288.208C457.157 288.208 456.518 287.569 456.518 286.781C456.518 285.993 457.157 285.354 457.945 285.354C458.733 285.354 459.372 285.993 459.372 286.781C459.372 287.569 458.733 288.208 457.945 288.208Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M487.19 291.059C486.402 291.059 485.763 290.42 485.763 289.633C485.763 288.845 486.402 288.207 487.19 288.207C487.978 288.207 488.617 288.845 488.617 289.633C488.617 290.42 487.978 291.059 487.19 291.059Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M239.924 475.15C216.859 475.15 198.161 456.452 198.161 433.387C198.161 410.322 216.859 391.624 239.924 391.624C262.989 391.624 281.687 410.322 281.687 433.387C281.687 456.452 262.989 475.15 239.924 475.15Z\"\n        fill=\"#D53B1C\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M214.18 466.264C208.958 459.141 205.866 450.31 205.866 440.748C205.866 417.165 224.679 398.019 247.85 398.019C256.005 398.019 263.621 400.39 270.066 404.492C277.262 411.996 281.687 422.179 281.687 433.387C281.687 456.437 262.973 475.15 239.924 475.15C230.215 475.15 221.275 471.83 214.18 466.264Z\"\n        fill=\"#E84827\"\n      />\n      <path\n        d=\"M239.924 475.15C216.859 475.15 198.161 456.452 198.161 433.387C198.161 410.322 216.859 391.624 239.924 391.624C262.989 391.624 281.687 410.322 281.687 433.387C281.687 456.452 262.989 475.15 239.924 475.15Z\"\n        stroke=\"#8D3A2B\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M235.231 416.378C231.977 416.378 229.34 413.74 229.34 410.487C229.34 407.233 231.977 404.596 235.231 404.596C238.484 404.596 241.122 407.233 241.122 410.487C241.122 413.74 238.484 416.378 235.231 416.378Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M253.894 460.01C248.897 460.01 244.847 455.959 244.847 450.963C244.847 445.966 248.897 441.916 253.894 441.916C258.89 441.916 262.941 445.966 262.941 450.963C262.941 455.959 258.89 460.01 253.894 460.01Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M259.785 425.18C254.924 425.18 250.984 421.239 250.984 416.379C250.984 411.518 254.924 407.578 259.785 407.578C264.646 407.578 268.586 411.518 268.586 416.379C268.586 421.239 264.646 425.18 259.785 425.18Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M214.701 428.888C212.264 428.888 210.289 426.912 210.289 424.476C210.289 422.039 212.264 420.064 214.701 420.064C217.138 420.064 219.113 422.039 219.113 424.476C219.113 426.912 217.138 428.888 214.701 428.888Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M237.242 464.825C235.623 464.825 234.311 463.513 234.311 461.894C234.311 460.275 235.623 458.963 237.242 458.963C238.861 458.963 240.173 460.275 240.173 461.894C240.173 463.513 238.861 464.825 237.242 464.825Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M243.104 424.102C241.485 424.102 240.173 422.789 240.173 421.171C240.173 419.552 241.485 418.24 243.104 418.24C244.723 418.24 246.035 419.552 246.035 421.171C246.035 422.789 244.723 424.102 243.104 424.102Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M226.257 458.963C220.19 458.963 215.272 454.045 215.272 447.979C215.272 441.912 220.19 436.995 226.257 436.995C232.324 436.995 237.242 441.912 237.242 447.979C237.242 454.045 232.324 458.963 226.257 458.963Z\"\n        fill=\"#F5A27F\"\n      />\n      <path\n        d=\"M246.908 436.095C245.413 436.095 244.201 434.883 244.201 433.388C244.201 431.893 245.413 430.681 246.908 430.681C248.403 430.681 249.615 431.893 249.615 433.388C249.615 434.883 248.403 436.095 246.908 436.095Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M269.92 430.681C268.424 430.681 267.212 429.468 267.212 427.973C267.212 426.477 268.424 425.265 269.92 425.265C271.416 425.265 272.628 426.477 272.628 427.973C272.628 429.468 271.416 430.681 269.92 430.681Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M228.434 425.265C226.939 425.265 225.727 424.053 225.727 422.558C225.727 421.063 226.939 419.851 228.434 419.851C229.929 419.851 231.141 421.063 231.141 422.558C231.141 424.053 229.929 425.265 228.434 425.265Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M211.692 438.802C210.197 438.802 208.985 437.59 208.985 436.095C208.985 434.6 210.197 433.388 211.692 433.388C213.187 433.388 214.399 434.6 214.399 436.095C214.399 437.59 213.187 438.802 211.692 438.802Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        d=\"M267.211 444.217C265.716 444.217 264.504 443.005 264.504 441.51C264.504 440.015 265.716 438.803 267.211 438.803C268.706 438.803 269.918 440.015 269.918 441.51C269.918 443.005 268.706 444.217 267.211 444.217Z\"\n        fill=\"#B73D2D\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M83.1774 188.347C83.1774 188.347 91.1947 216.836 99.2287 223.519C106.347 229.44 116.933 228.469 122.853 221.349C128.775 214.231 127.803 203.645 120.684 197.724C112.651 191.042 83.1774 188.347 83.1774 188.347Z\"\n        fill=\"#94E281\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M83.2629 188.647C83.2069 188.451 83.1769 188.347 83.1769 188.347C83.1769 188.347 112.651 191.042 120.685 197.724C127.803 203.645 128.775 214.231 122.854 221.349C122.817 221.394 122.779 221.439 122.741 221.483L83.2629 188.647Z\"\n        fill=\"#4BB74C\"\n      />\n      <path\n        d=\"M83.1774 188.347C83.1774 188.347 91.1947 216.836 99.2287 223.519C106.347 229.44 116.933 228.469 122.853 221.349C128.775 214.231 127.803 203.645 120.684 197.724C112.651 191.042 83.1774 188.347 83.1774 188.347Z\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M122.854 221.348L132.92 229.722\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M101.148 203.295L122.854 221.349\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M101.062 213.004L111.652 212.031\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M108.699 219.356L119.289 218.384\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M110.679 201.442L111.652 212.031\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M118.316 207.794L119.289 218.384\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M324.389 148.623C324.389 148.623 294.505 170.91 290.272 183.373C286.522 194.415 292.443 206.425 303.487 210.175C314.53 213.925 326.54 208.006 330.29 196.963C334.521 184.499 324.389 148.623 324.389 148.623Z\"\n        fill=\"#94E281\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M324.075 148.86C324.28 148.705 324.389 148.624 324.389 148.624C324.389 148.624 334.522 184.499 330.29 196.963C326.54 208.006 314.53 213.926 303.487 210.176C303.417 210.152 303.348 210.128 303.278 210.104L324.075 148.86Z\"\n        fill=\"#4BB74C\"\n      />\n      <path\n        d=\"M324.389 148.623C324.389 148.623 294.505 170.91 290.272 183.373C286.522 194.415 292.443 206.425 303.487 210.175C314.53 213.925 326.54 208.006 330.29 196.963C334.521 184.499 324.389 148.623 324.389 148.623Z\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M303.487 210.176L298.184 225.791\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M314.921 176.502L303.488 210.175\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M303.467 180.783L309.39 192.797\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M299.443 192.631L305.365 204.645\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M321.402 186.874L309.39 192.797\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M317.378 198.722L305.365 204.645\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M519.787 217.031C519.787 217.031 487.407 210.404 476.663 214.957C467.143 218.99 462.689 229.994 466.723 239.515C470.757 249.034 481.761 253.489 491.281 249.454C502.026 244.902 519.787 217.031 519.787 217.031Z\"\n        fill=\"#94E281\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M519.446 216.963C519.669 217.007 519.786 217.031 519.786 217.031C519.786 217.031 502.025 244.902 491.281 249.455C481.761 253.489 470.757 249.035 466.723 239.515C466.697 239.455 466.672 239.394 466.647 239.334L519.446 216.963Z\"\n        fill=\"#4BB74C\"\n      />\n      <path\n        d=\"M519.787 217.031C519.787 217.031 487.407 210.404 476.663 214.957C467.143 218.99 462.689 229.994 466.723 239.515C470.757 249.034 481.761 253.489 491.281 249.454C502.026 244.902 519.787 217.031 519.787 217.031Z\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M466.723 239.515L453.26 245.219\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.17\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M495.752 227.215L466.723 239.516\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M486.16 222.159L481.704 233.166\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M475.947 226.488L471.491 237.494\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M492.716 237.622L481.709 233.166\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M482.498 241.95L471.491 237.495\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M266.791 509.72C266.791 509.72 287.805 490.366 290.045 480.531C292.028 471.817 286.565 463.132 277.851 461.147C269.137 459.163 260.451 464.627 258.467 473.341C256.227 483.176 266.791 509.72 266.791 509.72Z\"\n        fill=\"#94E281\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M267.011 509.515C266.867 509.649 266.791 509.72 266.791 509.72C266.791 509.72 256.227 483.176 258.467 473.341C260.451 464.627 269.137 459.163 277.851 461.147C277.906 461.16 277.961 461.172 278.016 461.186L267.011 509.515Z\"\n        fill=\"#4BB74C\"\n      />\n      <path\n        d=\"M266.791 509.72C266.791 509.72 287.805 490.366 290.045 480.531C292.028 471.817 286.565 463.132 277.851 461.147C269.137 459.163 260.451 464.627 258.467 473.341C256.227 483.176 266.791 509.72 266.791 509.72Z\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M277.851 461.147L280.657 448.825\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M271.8 487.719L277.851 461.147\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M280.193 483.549L274.728 474.862\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M282.322 474.201L276.857 465.512\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M266.04 480.327L274.728 474.862\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M268.169 470.978L276.857 465.512\"\n        stroke=\"#3F6B29\"\n        strokeWidth=\"5.18\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M267.355 175.999C242.663 180.625 218.896 164.359 214.27 139.666C209.644 114.974 225.911 91.2071 250.603 86.5811C275.295 81.9551 299.062 98.2219 303.688 122.914C308.314 147.606 292.047 171.373 267.355 175.999Z\"\n        fill=\"#F16A65\"\n        stroke=\"#C72828\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M259.483 139.301C259.633 138.934 259.958 138.668 260.348 138.595C260.737 138.522 261.137 138.652 261.409 138.94C265.252 143.005 276.929 155.357 281.061 159.727C281.354 160.037 281.454 160.483 281.322 160.888C281.19 161.294 280.847 161.595 280.428 161.674C274.247 162.832 256.165 166.219 249.983 167.377C249.564 167.456 249.135 167.299 248.866 166.969C248.596 166.638 248.528 166.187 248.689 165.792C250.959 160.222 257.372 144.482 259.483 139.301Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M251.856 134.657C252.249 134.603 252.642 134.752 252.9 135.053C253.158 135.354 253.245 135.765 253.132 136.145C251.533 141.506 246.674 157.793 244.955 163.557C244.833 163.966 244.498 164.275 244.081 164.364C243.664 164.452 243.231 164.306 242.954 163.982C238.86 159.208 226.885 145.242 222.791 140.468C222.514 140.144 222.435 139.695 222.586 139.296C222.738 138.897 223.095 138.613 223.517 138.555C229.476 137.735 246.314 135.419 251.856 134.657Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M258.377 122.753C258.227 123.12 257.901 123.386 257.512 123.459C257.122 123.532 256.723 123.402 256.45 123.114C252.607 119.049 240.931 106.697 236.799 102.327C236.506 102.017 236.406 101.572 236.538 101.166C236.669 100.76 237.012 100.459 237.431 100.381C243.613 99.2218 261.695 95.8348 267.877 94.6768C268.296 94.5978 268.724 94.7547 268.994 95.0847C269.264 95.4157 269.332 95.8667 269.171 96.2617C266.901 101.832 260.488 117.572 258.377 122.753Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M265.634 125.425C265.242 125.479 264.848 125.33 264.591 125.029C264.333 124.728 264.245 124.317 264.358 123.937C265.957 118.576 270.816 102.288 272.535 96.5248C272.657 96.1158 272.992 95.8068 273.409 95.7178C273.827 95.6288 274.259 95.7758 274.537 96.0988C278.63 100.874 290.605 114.84 294.699 119.614C294.976 119.938 295.055 120.387 294.904 120.786C294.752 121.185 294.395 121.469 293.973 121.527C288.015 122.347 271.176 124.663 265.634 125.425Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M251.303 126.383C251.546 126.696 251.613 127.111 251.482 127.485C251.35 127.858 251.038 128.14 250.652 128.232C245.21 129.527 228.675 133.464 222.825 134.857C222.41 134.955 221.974 134.82 221.688 134.503C221.403 134.186 221.314 133.738 221.455 133.336C223.543 127.403 229.65 110.05 231.738 104.118C231.88 103.715 232.23 103.422 232.651 103.354C233.072 103.286 233.497 103.453 233.758 103.79C237.447 108.54 247.872 121.965 251.303 126.383Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M266.187 133.699C265.944 133.386 265.877 132.971 266.008 132.597C266.14 132.223 266.452 131.942 266.838 131.85C272.28 130.555 288.815 126.618 294.666 125.225C295.081 125.127 295.516 125.262 295.802 125.579C296.087 125.896 296.177 126.344 296.035 126.746C293.947 132.678 287.84 150.032 285.752 155.964C285.61 156.367 285.26 156.659 284.839 156.728C284.418 156.796 283.994 156.629 283.732 156.292C280.043 151.542 269.618 138.117 266.187 133.699Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        d=\"M256.683 119.035C255.055 119.34 253.338 117.47 252.849 114.858C252.36 112.247 253.284 109.882 254.912 109.578C256.54 109.273 258.256 111.143 258.745 113.754C259.234 116.366 258.311 118.73 256.683 119.035Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M262.579 150.503C260.951 150.808 259.234 148.938 258.745 146.327C258.255 143.715 259.178 141.351 260.806 141.046C262.434 140.741 264.151 142.61 264.64 145.222C265.13 147.834 264.206 150.198 262.579 150.503Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M268.468 121.806C267.389 120.548 268.151 118.127 270.168 116.398C272.185 114.668 274.694 114.285 275.772 115.543C276.85 116.8 276.089 119.221 274.072 120.951C272.055 122.68 269.546 123.063 268.468 121.806Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M242.26 144.259C241.181 143.001 241.943 140.58 243.96 138.85C245.977 137.121 248.486 136.738 249.564 137.995C250.642 139.253 249.881 141.674 247.864 143.403C245.847 145.133 243.338 145.516 242.26 144.259Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M238.199 122.582C238.749 121.019 241.227 120.468 243.733 121.35C246.239 122.232 247.825 124.214 247.276 125.776C246.726 127.338 244.248 127.89 241.742 127.008C239.235 126.126 237.649 124.144 238.199 122.582Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M270.757 134.026C271.307 132.463 273.785 131.912 276.291 132.794C278.797 133.676 280.384 135.658 279.834 137.22C279.284 138.782 276.806 139.334 274.3 138.452C271.794 137.57 270.208 135.588 270.757 134.026Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M359.724 471.535C335.033 476.161 311.266 459.895 306.64 435.203C302.014 410.512 318.281 386.745 342.973 382.119C367.664 377.493 391.431 393.76 396.057 418.451C400.682 443.143 384.416 466.909 359.724 471.535Z\"\n        fill=\"#F16A65\"\n        stroke=\"#C72828\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M351.852 434.838C352.002 434.471 352.327 434.205 352.717 434.132C353.106 434.059 353.506 434.189 353.778 434.477C357.621 438.542 369.298 450.893 373.43 455.264C373.723 455.574 373.823 456.019 373.691 456.425C373.559 456.83 373.217 457.131 372.797 457.21C366.616 458.368 348.534 461.756 342.352 462.914C341.933 462.992 341.504 462.836 341.235 462.505C340.965 462.175 340.897 461.723 341.058 461.328C343.328 455.759 349.741 440.018 351.852 434.838Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M344.225 430.193C344.618 430.139 345.011 430.289 345.269 430.589C345.527 430.89 345.614 431.302 345.501 431.681C343.902 437.042 339.044 453.33 337.324 459.093C337.203 459.502 336.867 459.811 336.45 459.9C336.033 459.989 335.6 459.843 335.323 459.519C331.229 454.744 319.254 440.779 315.161 436.004C314.883 435.681 314.804 435.231 314.956 434.832C315.107 434.433 315.464 434.149 315.887 434.091C321.845 433.272 338.683 430.956 344.225 430.193Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M350.746 418.289C350.596 418.656 350.271 418.922 349.881 418.995C349.492 419.068 349.092 418.938 348.82 418.65C344.977 414.585 333.3 402.234 329.168 397.863C328.875 397.553 328.775 397.108 328.907 396.702C329.039 396.297 329.381 395.995 329.801 395.917C335.982 394.759 354.064 391.371 360.246 390.213C360.665 390.135 361.094 390.291 361.363 390.622C361.633 390.952 361.701 391.404 361.54 391.798C359.27 397.368 352.857 413.109 350.746 418.289Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M358.003 420.961C357.611 421.015 357.218 420.866 356.96 420.565C356.702 420.264 356.614 419.853 356.728 419.473C358.327 414.113 363.185 397.825 364.904 392.061C365.026 391.653 365.361 391.343 365.779 391.255C366.196 391.166 366.628 391.312 366.906 391.636C370.999 396.41 382.974 410.376 387.068 415.15C387.346 415.474 387.424 415.924 387.273 416.322C387.122 416.721 386.764 417.005 386.342 417.064C380.384 417.883 363.545 420.199 358.003 420.961Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M343.672 421.919C343.915 422.232 343.982 422.647 343.851 423.021C343.719 423.395 343.407 423.676 343.021 423.768C337.579 425.064 321.044 429 315.194 430.393C314.779 430.492 314.343 430.356 314.058 430.039C313.772 429.722 313.683 429.275 313.824 428.872C315.912 422.94 322.019 405.586 324.107 399.654C324.249 399.252 324.599 398.959 325.02 398.89C325.441 398.822 325.866 398.989 326.127 399.326C329.816 404.076 340.241 417.501 343.672 421.919Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M358.556 429.235C358.313 428.922 358.246 428.507 358.378 428.134C358.509 427.76 358.822 427.478 359.207 427.387C364.649 426.091 381.184 422.155 387.035 420.762C387.45 420.663 387.885 420.799 388.171 421.116C388.456 421.433 388.546 421.88 388.404 422.282C386.316 428.215 380.209 445.568 378.121 451.501C377.98 451.903 377.63 452.196 377.209 452.264C376.788 452.332 376.363 452.165 376.101 451.829C372.412 447.078 361.988 433.654 358.556 429.235Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        d=\"M349.053 414.572C347.425 414.877 345.708 413.007 345.219 410.395C344.73 407.784 345.654 405.419 347.282 405.114C348.91 404.81 350.626 406.68 351.115 409.291C351.604 411.903 350.681 414.267 349.053 414.572Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M354.948 446.039C353.32 446.344 351.603 444.474 351.114 441.863C350.625 439.251 351.547 436.887 353.175 436.582C354.803 436.277 356.52 438.146 357.009 440.758C357.499 443.37 356.576 445.734 354.948 446.039Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M360.837 417.343C359.759 416.085 360.52 413.664 362.537 411.934C364.554 410.205 367.063 409.822 368.141 411.079C369.22 412.337 368.458 414.758 366.441 416.488C364.424 418.217 361.915 418.6 360.837 417.343Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M334.629 439.795C333.551 438.538 334.312 436.117 336.329 434.387C338.346 432.658 340.855 432.275 341.933 433.532C343.011 434.789 342.25 437.211 340.233 438.94C338.216 440.67 335.707 441.053 334.629 439.795Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M330.568 418.118C331.118 416.556 333.596 416.004 336.102 416.887C338.608 417.769 340.194 419.75 339.645 421.313C339.095 422.875 336.617 423.426 334.111 422.544C331.605 421.662 330.018 419.681 330.568 418.118Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M363.125 429.561C363.675 427.999 366.153 427.447 368.659 428.329C371.165 429.211 372.752 431.193 372.202 432.755C371.652 434.318 369.174 434.869 366.668 433.987C364.162 433.105 362.576 431.124 363.125 429.561Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M402.943 226.607C387.566 213.729 385.541 190.825 398.418 175.449C411.296 160.073 434.2 158.047 449.576 170.925C464.953 183.803 466.979 206.707 454.101 222.083C441.224 237.459 418.319 239.485 402.943 226.607Z\"\n        fill=\"#F16A65\"\n        stroke=\"#C72828\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M421.228 203.248C421.606 203.13 422.019 203.213 422.323 203.467C422.626 203.722 422.78 204.113 422.731 204.506C422.15 209.128 420.605 221.416 419.981 226.376C419.928 226.8 419.648 227.16 419.251 227.316C418.854 227.472 418.403 227.398 418.076 227.124C414.065 223.765 403.789 215.159 399.778 211.8C399.451 211.526 399.299 211.095 399.383 210.677C399.467 210.259 399.773 209.92 400.18 209.793C404.953 208.309 416.78 204.631 421.228 203.248Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M420.303 195.834C420.594 196.103 420.729 196.501 420.661 196.891C420.592 197.282 420.33 197.61 419.965 197.764C415.672 199.572 404.257 204.378 399.65 206.318C399.257 206.483 398.805 206.421 398.471 206.155C398.138 205.889 397.976 205.463 398.05 205.042C398.953 199.889 401.268 186.686 402.172 181.533C402.246 181.113 402.543 180.767 402.947 180.63C403.351 180.494 403.798 180.589 404.111 180.878C407.783 184.27 416.882 192.673 420.303 195.834Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M431.567 193.955C431.188 194.073 430.776 193.99 430.472 193.736C430.169 193.481 430.015 193.09 430.064 192.697C430.645 188.075 432.19 175.787 432.814 170.827C432.867 170.404 433.147 170.043 433.544 169.887C433.941 169.731 434.392 169.805 434.719 170.079C438.73 173.438 449.006 182.044 453.017 185.404C453.344 185.677 453.496 186.108 453.412 186.526C453.328 186.944 453.022 187.283 452.615 187.41C447.842 188.894 436.015 192.572 431.567 193.955Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M433.52 200.141C433.229 199.872 433.095 199.474 433.163 199.084C433.231 198.693 433.494 198.365 433.859 198.211C438.152 196.403 449.566 191.597 454.174 189.657C454.567 189.491 455.019 189.554 455.352 189.82C455.686 190.085 455.848 190.512 455.774 190.932C454.87 196.085 452.555 209.289 451.651 214.442C451.578 214.862 451.281 215.208 450.876 215.345C450.472 215.481 450.026 215.386 449.713 215.097C446.04 211.705 436.942 203.302 433.52 200.141Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M425.473 191.187C425.385 191.574 425.108 191.89 424.735 192.026C424.363 192.161 423.947 192.099 423.632 191.86C419.92 189.046 410.05 181.563 406.066 178.543C405.726 178.286 405.554 177.863 405.618 177.441C405.681 177.019 405.97 176.666 406.371 176.52C411.285 174.726 423.877 170.129 428.792 168.335C429.193 168.189 429.641 168.273 429.961 168.555C430.282 168.836 430.422 169.271 430.328 169.687C429.227 174.563 426.499 186.644 425.473 191.187Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M428.351 204.787C428.438 204.401 428.716 204.085 429.088 203.949C429.46 203.813 429.876 203.876 430.192 204.115C433.904 206.929 443.774 214.412 447.757 217.432C448.097 217.689 448.269 218.112 448.206 218.534C448.142 218.956 447.853 219.309 447.453 219.455C442.538 221.249 429.946 225.846 425.032 227.64C424.631 227.786 424.183 227.702 423.862 227.42C423.542 227.138 423.401 226.704 423.495 226.288C424.596 221.412 427.325 209.331 428.351 204.787Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        d=\"M432.652 191.135C431.638 190.286 431.921 188.279 433.283 186.653C434.645 185.027 436.571 184.396 437.584 185.245C438.598 186.094 438.316 188.101 436.954 189.727C435.592 191.353 433.666 191.984 432.652 191.135Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M416.241 210.73C415.227 209.881 415.509 207.874 416.871 206.248C418.233 204.622 420.159 203.991 421.173 204.84C422.187 205.689 421.904 207.696 420.542 209.322C419.18 210.948 417.254 211.579 416.241 210.73Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M436.921 199.805C437.149 198.502 439.028 197.743 441.118 198.11C443.207 198.476 444.716 199.829 444.487 201.132C444.259 202.435 442.38 203.194 440.29 202.828C438.201 202.461 436.692 201.108 436.921 199.805Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M409.783 195.035C410.012 193.732 411.891 192.973 413.98 193.339C416.07 193.706 417.578 195.059 417.35 196.362C417.121 197.665 415.242 198.424 413.153 198.057C411.063 197.691 409.555 196.338 409.783 195.035Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M421.089 181.535C422.331 181.082 423.927 182.33 424.655 184.323C425.382 186.315 424.965 188.298 423.723 188.751C422.481 189.205 420.884 187.957 420.157 185.964C419.43 183.972 419.847 181.989 421.089 181.535Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M430.548 207.415C431.79 206.961 433.387 208.209 434.115 210.201C434.842 212.194 434.425 214.177 433.182 214.631C431.939 215.084 430.343 213.837 429.615 211.844C428.888 209.851 429.305 207.868 430.548 207.415Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M165.966 423.409C144.291 418.016 131.093 396.073 136.487 374.398C141.88 352.724 163.823 339.526 185.498 344.919C207.172 350.313 220.371 372.256 214.977 393.93C209.583 415.605 187.64 428.803 165.966 423.409Z\"\n        fill=\"#F16A65\"\n        stroke=\"#C72828\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M173.03 390.964C173.319 390.693 173.725 390.586 174.11 390.682C174.495 390.778 174.804 391.062 174.932 391.437C176.573 396.258 181.226 409.921 182.989 415.099C183.127 415.502 183.033 415.949 182.744 416.263C182.455 416.577 182.018 416.708 181.604 416.605C176.052 415.223 160.892 411.451 155.34 410.069C154.926 409.966 154.601 409.645 154.494 409.232C154.386 408.82 154.512 408.381 154.823 408.089C158.807 404.342 169.321 394.453 173.03 390.964Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M168.536 384.193C168.915 384.307 169.21 384.606 169.32 384.987C169.429 385.368 169.338 385.778 169.077 386.076C165.723 389.908 156.217 400.769 152.614 404.885C152.333 405.206 151.9 405.348 151.483 405.255C151.067 405.162 150.735 404.849 150.617 404.439C149.037 398.939 144.725 383.924 143.145 378.425C143.027 378.015 143.143 377.573 143.447 377.274C143.75 376.974 144.193 376.864 144.601 376.987C149.839 378.564 163.66 382.725 168.536 384.193Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M178.549 376.901C178.261 377.172 177.854 377.278 177.469 377.183C177.085 377.087 176.775 376.802 176.648 376.427C175.006 371.607 170.353 357.944 168.59 352.766C168.453 352.363 168.547 351.916 168.835 351.602C169.124 351.288 169.561 351.157 169.975 351.26C175.528 352.641 190.687 356.414 196.24 357.796C196.654 357.899 196.978 358.22 197.086 358.632C197.194 359.045 197.067 359.484 196.757 359.776C192.773 363.523 182.259 373.412 178.549 376.901Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M183.475 381.941C183.095 381.827 182.8 381.527 182.69 381.147C182.581 380.766 182.673 380.355 182.934 380.057C186.287 376.226 195.794 365.365 199.396 361.249C199.677 360.928 200.111 360.786 200.527 360.879C200.943 360.972 201.276 361.285 201.393 361.695C202.973 367.194 207.285 382.209 208.865 387.709C208.983 388.119 208.867 388.56 208.564 388.86C208.26 389.159 207.817 389.269 207.409 389.146C202.171 387.57 188.351 383.409 183.475 381.941Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M171.295 377.161C171.386 377.547 171.275 377.952 171 378.237C170.724 378.522 170.323 378.648 169.935 378.571C164.939 377.583 150.78 374.781 145.415 373.719C144.996 373.636 144.657 373.331 144.529 372.924C144.401 372.517 144.506 372.073 144.802 371.766C148.775 367.648 159.622 356.406 163.595 352.288C163.891 351.981 164.331 351.861 164.743 351.974C165.154 352.087 165.471 352.415 165.568 352.831C166.822 358.155 170.129 372.204 171.295 377.161Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M180.715 388.973C180.624 388.587 180.736 388.182 181.011 387.896C181.286 387.611 181.687 387.485 182.076 387.562C187.071 388.551 201.23 391.353 206.595 392.415C207.014 392.498 207.354 392.802 207.481 393.209C207.609 393.616 207.504 394.061 207.208 394.368C203.235 398.485 192.388 409.728 188.415 413.845C188.119 414.152 187.679 414.273 187.268 414.16C186.856 414.047 186.54 413.718 186.442 413.303C185.189 407.979 181.882 393.929 180.715 388.973Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        d=\"M178.409 373.406C176.979 373.051 176.283 370.904 176.854 368.612C177.424 366.32 179.045 364.75 180.475 365.106C181.904 365.461 182.6 367.608 182.03 369.9C181.459 372.192 179.838 373.762 178.409 373.406Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M171.534 401.028C170.105 400.672 169.409 398.526 169.979 396.233C170.55 393.941 172.171 392.371 173.6 392.727C175.03 393.083 175.726 395.229 175.156 397.522C174.585 399.814 172.964 401.384 171.534 401.028Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M186.912 380.005C186.506 378.589 188.017 376.913 190.287 376.261C192.557 375.609 194.727 376.228 195.134 377.644C195.54 379.06 194.03 380.736 191.759 381.388C189.489 382.04 187.319 381.421 186.912 380.005Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M157.418 388.464C157.011 387.049 158.522 385.372 160.792 384.72C163.062 384.068 165.232 384.687 165.639 386.102C166.046 387.518 164.535 389.194 162.265 389.847C159.995 390.499 157.824 389.88 157.418 388.464Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M162.153 369.436C163.176 368.376 165.383 368.847 167.083 370.487C168.783 372.127 169.332 374.316 168.309 375.376C167.286 376.436 165.079 375.965 163.379 374.325C161.679 372.685 161.131 370.496 162.153 369.436Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M184.243 390.732C185.266 389.672 187.473 390.142 189.173 391.782C190.873 393.422 191.422 395.611 190.399 396.671C189.377 397.731 187.169 397.261 185.469 395.621C183.77 393.981 183.22 391.792 184.243 390.732Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M162.177 131.54L168.13 150.073C168.13 150.073 170.447 157.625 177.334 154.909C182.565 152.846 182.99 148.57 181.76 145.047C180.393 141.127 175.537 127.054 175.537 127.054C175.537 127.054 185.189 131.1 188.777 131.12C192.063 131.138 195.943 128.881 196.774 126.392C198.969 119.827 186.535 111.153 173.567 110.686C160.6 110.22 148.086 118.027 144.238 128.855C139.955 140.908 144.532 145.924 149.953 144.481C155.374 143.038 161.211 133.608 162.177 131.54Z\"\n        fill=\"#E2BC9B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M144.881 143.57C144.95 141.397 145.466 138.772 146.563 135.685C150.411 124.857 162.925 117.049 175.892 117.516C184.459 117.824 192.793 121.714 196.83 126.212C196.813 126.272 196.794 126.332 196.774 126.392C195.943 128.881 192.063 131.138 188.777 131.12C185.189 131.1 175.537 127.054 175.537 127.054C175.537 127.054 176.616 130.182 177.901 133.899L177.862 133.883C177.862 133.883 180.162 140.549 182.042 145.984C182.879 149.299 182.109 153.026 177.334 154.909C172.298 156.895 169.705 153.391 168.676 151.363L164.502 138.369C164.479 138.419 164.452 138.474 164.423 138.532L162.177 131.54C161.211 133.608 155.374 143.038 149.953 144.481C148.04 144.99 146.232 144.695 144.881 143.57Z\"\n        fill=\"#ECCFB5\"\n      />\n      <path\n        d=\"M162.177 131.54L168.13 150.073C168.13 150.073 170.447 157.625 177.334 154.909C182.565 152.846 182.99 148.57 181.76 145.047C180.393 141.127 175.537 127.054 175.537 127.054C175.537 127.054 185.189 131.1 188.777 131.12C192.063 131.138 195.943 128.881 196.774 126.392C198.969 119.827 186.535 111.153 173.567 110.686C160.6 110.22 148.086 118.027 144.238 128.855C139.955 140.908 144.532 145.924 149.953 144.481C155.374 143.038 161.211 133.608 162.177 131.54Z\"\n        stroke=\"#9A7451\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M358.81 168.724L346.657 185.493C346.657 185.493 341.637 192.234 348.294 196.427C353.349 199.612 357.282 197.284 359.531 194.012C362.033 190.372 371.049 177.351 371.049 177.351C371.049 177.351 373.644 188.17 375.881 191.258C377.93 194.086 382.303 195.99 384.96 195.137C391.967 192.885 391.594 176.766 383.847 165.358C376.1 153.95 361.543 148.139 349.841 151.658C336.815 155.575 335.389 162.655 340.033 166.393C344.676 170.131 356.429 169.198 358.81 168.724Z\"\n        fill=\"#E2BC9B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M337.627 161.473C339.534 160.164 342.109 158.953 345.445 157.95C357.148 154.431 371.705 160.242 379.452 171.65C384.57 179.187 386.47 188.78 385.149 195.072C385.087 195.095 385.024 195.117 384.96 195.137C382.303 195.99 377.93 194.086 375.881 191.258C373.644 188.17 371.049 177.351 371.049 177.351C371.049 177.351 369.045 180.245 366.664 183.686L366.654 183.643C366.654 183.643 362.383 189.811 358.904 194.844C356.587 197.648 352.908 199.334 348.294 196.427C343.427 193.361 344.802 188.934 345.893 186.774L354.414 175.016C354.356 175.027 354.293 175.039 354.225 175.051L358.81 168.724C356.429 169.198 344.676 170.131 340.033 166.393C338.394 165.074 337.511 163.338 337.627 161.473Z\"\n        fill=\"#ECCFB5\"\n      />\n      <path\n        d=\"M358.81 168.724L346.657 185.493C346.657 185.493 341.637 192.234 348.294 196.427C353.349 199.612 357.282 197.284 359.531 194.012C362.033 190.372 371.049 177.351 371.049 177.351C371.049 177.351 373.644 188.17 375.881 191.258C377.93 194.086 382.303 195.99 384.96 195.137C391.967 192.885 391.594 176.766 383.847 165.358C376.1 153.95 361.543 148.139 349.841 151.658C336.815 155.575 335.389 162.655 340.033 166.393C344.676 170.131 356.429 169.198 358.81 168.724Z\"\n        stroke=\"#9A7451\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M170.405 247.004L182.652 262.587C182.652 262.587 187.539 268.976 193.144 263.949C197.401 260.132 196.29 255.902 193.869 252.974C191.175 249.716 181.556 238.002 181.556 238.002C181.556 238.002 192.194 238.452 195.623 237.203C198.763 236.059 201.663 232.534 201.574 229.865C201.339 222.824 186.407 218.94 173.875 223.078C161.343 227.216 152.177 239.089 152.346 250.782C152.534 263.798 158.677 266.967 163.335 263.675C167.993 260.382 170.217 249.32 170.405 247.004Z\"\n        fill=\"#E2BC9B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M158.175 264.597C157.471 262.499 157.032 259.811 156.984 256.478C156.815 244.785 165.981 232.911 178.513 228.774C186.792 226.04 196.12 226.808 201.564 229.674C201.569 229.737 201.572 229.801 201.574 229.865C201.663 232.534 198.763 236.059 195.623 237.203C192.194 238.452 181.556 238.002 181.556 238.002C181.556 238.002 183.694 240.606 186.237 243.7L186.194 243.698C186.194 243.698 190.75 249.247 194.47 253.768C196.443 256.636 197.03 260.465 193.144 263.949C189.044 267.626 185.329 265.195 183.629 263.624L175.043 252.7C175.038 252.756 175.033 252.817 175.026 252.883L170.405 247.004C170.217 249.32 167.993 260.382 163.335 263.675C161.691 264.837 159.862 265.194 158.175 264.597Z\"\n        fill=\"#ECCFB5\"\n      />\n      <path\n        d=\"M170.405 247.004L182.652 262.587C182.652 262.587 187.539 268.976 193.144 263.949C197.401 260.132 196.29 255.902 193.869 252.974C191.175 249.716 181.556 238.002 181.556 238.002C181.556 238.002 192.194 238.452 195.623 237.203C198.763 236.059 201.663 232.534 201.574 229.865C201.339 222.824 186.407 218.94 173.875 223.078C161.343 227.216 152.177 239.089 152.346 250.782C152.534 263.798 158.677 266.967 163.335 263.675C167.993 260.382 170.217 249.32 170.405 247.004Z\"\n        stroke=\"#9A7451\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M95.9959 362.242L111.886 355.716C111.886 355.716 118.368 353.152 115.499 347.282C113.32 342.824 109.529 342.748 106.516 344.075C103.162 345.551 91.1199 350.802 91.1199 350.802C91.1199 350.802 94.0079 342.03 93.7749 338.872C93.5619 335.98 91.3069 332.724 89.0599 332.166C83.1319 330.692 76.3659 342.234 76.8589 353.674C77.3509 365.114 85.0909 375.58 94.8839 378.21C105.785 381.139 109.879 376.764 108.232 372.095C106.585 367.427 97.8829 362.948 95.9959 362.242Z\"\n        fill=\"#E2BC9B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M88.8979 332.129C88.9519 332.14 89.0059 332.153 89.0599 332.166C91.3069 332.724 93.5619 335.98 93.7749 338.872C94.0079 342.03 91.1199 350.802 91.1199 350.802C91.1199 350.802 93.7969 349.635 96.9769 348.246L96.9659 348.281C96.9659 348.281 102.666 345.796 107.314 343.764C110.173 342.794 113.509 343.21 115.499 347.282C117.596 351.572 114.698 354.096 112.986 355.144L101.842 359.722C101.888 359.739 101.937 359.758 101.991 359.78L95.9959 362.242C97.8829 362.948 106.585 367.427 108.232 372.095C108.813 373.743 108.679 375.354 107.784 376.62C105.867 376.711 103.522 376.44 100.73 375.69C90.9369 373.059 83.1969 362.594 82.7049 351.154C82.3789 343.596 85.2219 335.994 88.8979 332.129Z\"\n        fill=\"#ECCFB5\"\n      />\n      <path\n        d=\"M95.9959 362.242L111.886 355.716C111.886 355.716 118.368 353.152 115.499 347.282C113.32 342.824 109.529 342.748 106.516 344.075C103.162 345.551 91.1199 350.802 91.1199 350.802C91.1199 350.802 94.0079 342.03 93.7749 338.872C93.5619 335.98 91.3069 332.724 89.0599 332.166C83.1319 330.692 76.3659 342.234 76.8589 353.674C77.3509 365.114 85.0909 375.58 94.8839 378.21C105.785 381.139 109.879 376.764 108.232 372.095C106.585 367.427 97.8829 362.948 95.9959 362.242Z\"\n        stroke=\"#9A7451\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M432.62 424.087L413.46 408.288C413.46 408.288 405.759 401.788 400.91 408.574C397.228 413.727 399.875 418.408 403.613 421.355C407.774 424.635 422.653 436.438 422.653 436.438C422.653 436.438 410.229 437.801 406.675 439.848C403.42 441.722 401.209 446.299 402.173 449.36C404.714 457.435 423.201 459.272 436.331 452.285C449.461 445.298 456.212 429.988 452.246 416.537C447.832 401.565 439.72 398.998 435.405 403.618C431.091 408.238 432.091 421.385 432.62 424.087Z\"\n        fill=\"#E2BC9B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M425.391 418.126L432.62 424.087C432.091 421.385 431.091 408.238 435.405 403.618C436.928 401.987 438.924 401.252 441.063 401.642C442.553 403.936 443.926 406.957 445.056 410.791C449.022 424.242 442.271 439.552 429.141 446.539C420.467 451.155 409.454 451.92 402.246 449.579C402.22 449.506 402.196 449.433 402.173 449.36C401.209 446.299 403.42 441.722 406.675 439.848C410.229 437.801 422.653 436.438 422.653 436.438C422.653 436.438 419.346 433.814 415.414 430.698L415.463 430.692C415.463 430.692 408.416 425.102 402.666 420.547C399.462 417.59 397.548 413.279 400.91 408.574C404.456 403.611 409.528 405.754 411.997 407.264L425.43 418.341C425.417 418.275 425.404 418.204 425.391 418.126Z\"\n        fill=\"#ECCFB5\"\n      />\n      <path\n        d=\"M432.62 424.087L413.46 408.288C413.46 408.288 405.759 401.788 400.91 408.574C397.228 413.727 399.875 418.408 403.613 421.355C407.774 424.635 422.653 436.438 422.653 436.438C422.653 436.438 410.229 437.801 406.675 439.848C403.42 441.722 401.209 446.299 402.173 449.36C404.714 457.435 423.201 459.272 436.331 452.285C449.461 445.298 456.212 429.988 452.246 416.537C447.832 401.565 439.72 398.998 435.405 403.618C431.091 408.238 432.091 421.385 432.62 424.087Z\"\n        stroke=\"#9A7451\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M365.521 236.851C358.686 236.851 353.146 230.065 353.146 221.694C353.146 213.323 358.686 206.537 365.521 206.537C372.355 206.537 377.896 213.323 377.896 221.694C377.896 230.065 372.355 236.851 365.521 236.851Z\"\n        fill=\"#6A895B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M368.94 207.124C374.11 208.942 377.897 214.78 377.897 221.694C377.897 229.6 372.943 236.101 366.633 236.791C362.401 234.166 359.494 228.753 359.494 222.508C359.494 215.183 363.494 209.003 368.94 207.124Z\"\n        fill=\"#5C784D\"\n      />\n      <path\n        d=\"M365.521 236.851C358.686 236.851 353.146 230.065 353.146 221.694C353.146 213.323 358.686 206.537 365.521 206.537C372.355 206.537 377.896 213.323 377.896 221.694C377.896 230.065 372.355 236.851 365.521 236.851Z\"\n        stroke=\"#374536\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M410.246 264.415C415.77 257.654 426.961 257.657 435.242 264.422C443.522 271.187 445.757 282.153 440.233 288.914C434.709 295.675 423.518 295.672 415.238 288.907C406.957 282.142 404.722 271.177 410.246 264.415Z\"\n        fill=\"#6A895B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M409.409 265.563C415.425 263.499 423.129 264.997 429.306 270.044C436.552 275.964 439.433 284.915 436.891 291.821C430.914 295.466 422.078 294.494 415.239 288.907C407.418 282.517 404.99 272.363 409.409 265.563Z\"\n        fill=\"#5C784D\"\n      />\n      <path\n        d=\"M410.246 264.415C415.77 257.654 426.961 257.657 435.242 264.422C443.522 271.187 445.757 282.153 440.233 288.914C434.709 295.675 423.518 295.672 415.238 288.907C406.957 282.142 404.722 271.177 410.246 264.415Z\"\n        stroke=\"#374536\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M308.041 476.524C313.564 469.763 324.755 469.766 333.036 476.531C341.316 483.296 343.551 494.261 338.028 501.022C332.504 507.783 321.313 507.78 313.033 501.015C304.752 494.25 302.517 483.285 308.041 476.524Z\"\n        fill=\"#6A895B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M307.202 477.672C313.219 475.608 320.922 477.107 327.099 482.154C334.345 488.073 337.226 497.025 334.684 503.931C328.707 507.575 319.871 506.603 313.032 501.016C305.211 494.626 302.783 484.472 307.202 477.672Z\"\n        fill=\"#5C784D\"\n      />\n      <path\n        d=\"M308.041 476.524C313.564 469.763 324.755 469.766 333.036 476.531C341.316 483.296 343.551 494.261 338.028 501.022C332.504 507.783 321.313 507.78 313.033 501.015C304.752 494.25 302.517 483.285 308.041 476.524Z\"\n        stroke=\"#374536\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M237.608 186.269C245.454 182.438 255.617 187.123 260.308 196.732C264.999 206.341 262.441 217.235 254.595 221.066C246.749 224.896 236.586 220.211 231.895 210.602C227.204 200.993 229.762 190.099 237.608 186.269Z\"\n        fill=\"#6A895B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M236.367 186.962C242.695 187.604 249.065 192.188 252.564 199.356C256.669 207.765 255.541 217.099 250.343 222.309C243.39 223.119 235.77 218.539 231.896 210.603C227.465 201.527 229.509 191.288 236.367 186.962Z\"\n        fill=\"#5C784D\"\n      />\n      <path\n        d=\"M237.608 186.269C245.454 182.438 255.617 187.123 260.308 196.732C264.999 206.341 262.441 217.235 254.595 221.066C246.749 224.896 236.586 220.211 231.895 210.602C227.204 200.993 229.762 190.099 237.608 186.269Z\"\n        stroke=\"#374536\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M122.332 386.965C129.237 392.309 129.528 403.496 122.983 411.952C116.438 420.407 105.535 422.93 98.6307 417.585C91.7264 412.241 91.4351 401.054 97.9801 392.598C104.525 384.143 115.428 381.62 122.332 386.965Z\"\n        fill=\"#6A895B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M95.6369 414.321C91.8359 408.442 92.5749 399.582 97.9809 392.599C104.163 384.612 114.25 381.92 121.163 386.157C123.385 392.118 122.09 399.858 117.207 406.166C111.48 413.565 102.608 416.68 95.6369 414.321Z\"\n        fill=\"#5C784D\"\n      />\n      <path\n        d=\"M122.332 386.965C129.237 392.309 129.528 403.496 122.983 411.952C116.438 420.407 105.535 422.93 98.6307 417.585C91.7264 412.241 91.4351 401.054 97.9801 392.598C104.525 384.143 115.428 381.62 122.332 386.965Z\"\n        stroke=\"#374536\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M488.905 193.696C491.052 189.072 489.044 183.583 484.42 181.436C483.223 180.879 481.99 180.307 480.793 179.751C476.169 177.604 470.68 179.611 468.532 184.235C465.778 190.167 462.569 197.076 462.569 197.076L482.942 206.537C482.942 206.537 486.15 199.628 488.905 193.696Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M469.664 200.371C469.68 200.336 472.879 193.447 475.627 187.53C477.498 183.5 481.907 181.458 486.07 182.421C489.491 184.98 490.776 189.666 488.905 193.696C486.15 199.628 482.942 206.537 482.942 206.537L469.664 200.371Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M488.905 193.696C491.052 189.072 489.044 183.583 484.42 181.436C483.223 180.879 481.99 180.307 480.793 179.751C476.169 177.604 470.68 179.611 468.532 184.235C465.778 190.167 462.569 197.076 462.569 197.076L482.942 206.537C482.942 206.537 486.15 199.628 488.905 193.696Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M216.181 183.89C216.181 178.792 212.048 174.659 206.95 174.659H202.95C197.852 174.659 193.719 178.792 193.719 183.89V198.048H216.181V183.89Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M208.861 174.857C213.042 175.738 216.181 179.447 216.181 183.89V198.048H201.542V183.89C201.542 179.447 204.681 175.738 208.861 174.857Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M216.181 183.89C216.181 178.792 212.048 174.659 206.95 174.659H202.95C197.852 174.659 193.719 178.792 193.719 183.89V198.048H216.181V183.89Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M176.701 305.501C171.996 303.538 166.591 305.762 164.628 310.468C164.12 311.686 163.597 312.94 163.089 314.159C161.127 318.865 163.35 324.27 168.056 326.232C174.093 328.749 181.123 331.681 181.123 331.681L189.769 310.95C189.769 310.95 182.738 308.018 176.701 305.501Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M165.546 308.781C167.968 305.26 172.6 303.79 176.701 305.501L189.768 310.95L185.772 320.533L184.134 324.462C184.087 324.442 177.083 321.522 171.067 319.012C166.966 317.302 164.75 312.978 165.546 308.781Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M176.701 305.501C171.996 303.538 166.591 305.762 164.628 310.468C164.12 311.686 163.597 312.94 163.089 314.159C161.127 318.865 163.35 324.27 168.056 326.232C174.093 328.749 181.123 331.681 181.123 331.681L189.769 310.95C189.769 310.95 182.738 308.018 176.701 305.501Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M477.383 344.613C481.978 346.822 487.493 344.889 489.703 340.294C490.275 339.104 490.864 337.88 491.436 336.69C493.646 332.095 491.712 326.579 487.118 324.37C481.223 321.535 474.358 318.234 474.358 318.234L464.624 338.477C464.624 338.477 471.489 341.778 477.383 344.613Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M464.624 338.477L470.968 325.284C470.968 325.284 477.833 328.585 483.728 331.42C487.732 333.345 489.715 337.782 488.696 341.931C486.09 345.317 481.387 346.538 477.383 344.613C471.497 341.782 464.643 338.486 464.624 338.477Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M477.383 344.613C481.978 346.822 487.493 344.889 489.703 340.294C490.275 339.104 490.864 337.88 491.436 336.69C493.646 332.095 491.712 326.579 487.118 324.37C481.223 321.535 474.358 318.234 474.358 318.234L464.624 338.477C464.624 338.477 471.489 341.778 477.383 344.613Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M170.822 443.618C167.097 447.098 166.898 452.939 170.379 456.665C171.28 457.63 172.208 458.623 173.109 459.588C176.59 463.313 182.431 463.511 186.156 460.031C190.936 455.566 196.502 450.366 196.502 450.366L181.168 433.952C181.168 433.952 175.602 439.153 170.822 443.618Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M181.168 433.952L191.162 444.65C191.162 444.65 185.596 449.85 180.816 454.315C177.57 457.348 172.717 457.587 169.219 455.133C167.007 451.477 167.576 446.651 170.822 443.618C175.593 439.161 181.147 433.972 181.168 433.952Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M170.822 443.618C167.097 447.098 166.898 452.939 170.379 456.665C171.28 457.63 172.208 458.623 173.109 459.588C176.59 463.313 182.431 463.511 186.156 460.031C190.936 455.566 196.502 450.366 196.502 450.366L181.168 433.952C181.168 433.952 175.602 439.153 170.822 443.618Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M340.739 354.135C342.935 358.736 348.445 360.685 353.046 358.489C354.238 357.92 355.464 357.334 356.656 356.765C361.257 354.569 363.206 349.059 361.01 344.458C358.192 338.556 354.91 331.681 354.91 331.681L334.64 341.358C334.64 341.358 337.921 348.232 340.739 354.135Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M347.851 335.051L353.951 347.828C355.865 351.838 354.63 356.538 351.236 359.133C347.083 360.14 342.653 358.145 340.739 354.135C337.921 348.232 334.64 341.358 334.64 341.358L347.851 335.051Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M340.739 354.135C342.935 358.736 348.445 360.685 353.046 358.489C354.238 357.92 355.464 357.334 356.656 356.765C361.257 354.569 363.206 349.059 361.01 344.458C358.192 338.556 354.91 331.681 354.91 331.681L334.64 341.358C334.64 341.358 337.921 348.232 340.739 354.135Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M211.483 85.1988C209.792 80.3898 204.522 77.8628 199.712 79.5538C198.466 79.9928 197.185 80.4428 195.939 80.8818C191.13 82.5728 188.602 87.8428 190.294 92.6528C192.464 98.8228 194.992 106.009 194.992 106.009L216.181 98.5548C216.181 98.5548 213.654 91.3688 211.483 85.1988Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M201.581 79.1068C205.818 78.5498 210.009 81.0078 211.483 85.1988C213.654 91.3688 216.181 98.5548 216.181 98.5548L205.949 102.154L202.371 103.413C202.371 103.413 199.843 96.2268 197.673 90.0568C196.198 85.8658 197.929 81.3248 201.581 79.1068Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M211.483 85.1988C209.792 80.3898 204.522 77.8628 199.712 79.5538C198.466 79.9928 197.185 80.4428 195.939 80.8818C191.13 82.5728 188.602 87.8428 190.294 92.6528C192.464 98.8228 194.992 106.009 194.992 106.009L216.181 98.5548C216.181 98.5548 213.654 91.3688 211.483 85.1988Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M422.158 141.526C426.05 138.233 426.536 132.409 423.243 128.517C422.39 127.508 421.513 126.471 420.66 125.463C417.367 121.571 411.542 121.085 407.65 124.378C402.657 128.603 396.842 133.523 396.842 133.523L411.35 150.671C411.35 150.671 417.165 145.751 422.158 141.526Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M411.35 150.671L401.894 139.494C401.894 139.494 407.709 134.574 412.703 130.35C416.095 127.48 420.954 127.48 424.326 130.104C426.355 133.864 425.55 138.656 422.158 141.526L411.35 150.671Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M422.158 141.526C426.05 138.233 426.536 132.409 423.243 128.517C422.39 127.508 421.513 126.471 420.66 125.463C417.367 121.571 411.542 121.085 407.65 124.378C402.657 128.603 396.842 133.523 396.842 133.523L411.35 150.671C411.35 150.671 417.165 145.751 422.158 141.526Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M315.552 76.5648C315.552 71.4668 311.419 67.3338 306.321 67.3338H302.321C297.223 67.3338 293.09 71.4668 293.09 76.5648V90.7228H315.552V76.5648Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M308.232 67.5317C312.413 68.4117 315.552 72.1217 315.552 76.5647V90.7227H300.913V76.5647C300.913 72.1217 304.051 68.4117 308.232 67.5317Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M315.552 76.5648C315.552 71.4668 311.419 67.3338 306.321 67.3338H302.321C297.223 67.3338 293.09 71.4668 293.09 76.5648V90.7228H315.552V76.5648Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M252.461 309.794C227.769 314.42 204.002 298.154 199.376 273.461C194.75 248.769 211.017 225.002 235.709 220.376C260.401 215.75 284.168 232.017 288.794 256.709C293.42 281.401 277.154 305.168 252.461 309.794Z\"\n        fill=\"#F16A65\"\n        stroke=\"#C72828\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M244.589 273.096C244.739 272.729 245.064 272.463 245.454 272.39C245.843 272.317 246.243 272.447 246.515 272.735C250.358 276.8 262.035 289.152 266.167 293.522C266.46 293.832 266.56 294.278 266.428 294.683C266.296 295.089 265.953 295.39 265.534 295.469C259.353 296.627 241.271 300.014 235.089 301.172C234.67 301.251 234.241 301.094 233.972 300.764C233.702 300.433 233.634 299.982 233.795 299.587C236.065 294.017 242.478 278.277 244.589 273.096Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M236.962 268.452C237.355 268.398 237.748 268.547 238.006 268.848C238.264 269.149 238.351 269.56 238.238 269.94C236.639 275.301 231.78 291.588 230.061 297.352C229.939 297.761 229.604 298.07 229.187 298.159C228.77 298.247 228.337 298.101 228.06 297.777C223.966 293.003 211.991 279.037 207.897 274.263C207.62 273.939 207.541 273.49 207.692 273.091C207.844 272.692 208.201 272.408 208.623 272.35C214.582 271.53 231.42 269.214 236.962 268.452Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M243.483 256.548C243.333 256.915 243.007 257.181 242.618 257.254C242.228 257.327 241.829 257.197 241.556 256.909C237.713 252.844 226.037 240.492 221.905 236.122C221.612 235.812 221.512 235.367 221.644 234.961C221.775 234.555 222.118 234.254 222.537 234.176C228.719 233.017 246.801 229.63 252.983 228.472C253.402 228.393 253.83 228.55 254.1 228.88C254.37 229.211 254.438 229.662 254.277 230.057C252.007 235.627 245.594 251.367 243.483 256.548Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M250.74 259.22C250.348 259.274 249.954 259.125 249.697 258.824C249.439 258.523 249.351 258.112 249.464 257.732C251.063 252.371 255.922 236.083 257.641 230.32C257.763 229.911 258.098 229.602 258.515 229.513C258.933 229.424 259.365 229.571 259.643 229.894C263.736 234.669 275.711 248.635 279.805 253.409C280.082 253.733 280.161 254.182 280.01 254.581C279.858 254.98 279.501 255.264 279.079 255.322C273.121 256.142 256.282 258.458 250.74 259.22Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M236.409 260.178C236.652 260.491 236.719 260.906 236.588 261.28C236.456 261.653 236.144 261.935 235.758 262.027C230.316 263.322 213.781 267.259 207.931 268.652C207.516 268.75 207.08 268.615 206.794 268.298C206.509 267.981 206.42 267.533 206.561 267.131C208.649 261.198 214.756 243.845 216.844 237.913C216.986 237.51 217.336 237.217 217.757 237.149C218.178 237.081 218.603 237.248 218.864 237.585C222.553 242.335 232.978 255.76 236.409 260.178Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M251.293 267.494C251.05 267.181 250.983 266.766 251.114 266.392C251.246 266.018 251.558 265.737 251.944 265.645C257.386 264.35 273.921 260.413 279.772 259.02C280.187 258.922 280.622 259.057 280.908 259.374C281.193 259.691 281.283 260.139 281.141 260.541C279.053 266.473 272.946 283.827 270.858 289.759C270.716 290.162 270.366 290.454 269.945 290.523C269.524 290.591 269.1 290.424 268.838 290.087C265.149 285.337 254.724 271.912 251.293 267.494Z\"\n        fill=\"#C72828\"\n      />\n      <path\n        d=\"M241.789 252.83C240.161 253.135 238.445 251.265 237.956 248.653C237.467 246.042 238.39 243.677 240.018 243.373C241.646 243.068 243.362 244.938 243.851 247.549C244.34 250.161 243.417 252.525 241.789 252.83Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M247.685 284.298C246.057 284.603 244.341 282.733 243.851 280.122C243.362 277.51 244.285 275.146 245.913 274.841C247.541 274.536 249.257 276.406 249.747 279.017C250.236 281.629 249.313 283.993 247.685 284.298Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M253.574 255.601C252.496 254.344 253.257 251.922 255.274 250.193C257.291 248.463 259.8 248.08 260.878 249.338C261.956 250.595 261.195 253.016 259.178 254.746C257.161 256.475 254.652 256.858 253.574 255.601Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M227.366 278.054C226.288 276.796 227.049 274.375 229.066 272.645C231.083 270.916 233.592 270.533 234.67 271.79C235.748 273.048 234.987 275.469 232.97 277.199C230.953 278.928 228.444 279.311 227.366 278.054Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M223.306 256.377C223.856 254.814 226.333 254.263 228.84 255.145C231.346 256.027 232.932 258.009 232.382 259.571C231.832 261.133 229.355 261.685 226.848 260.803C224.342 259.921 222.756 257.939 223.306 256.377Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        d=\"M255.864 267.821C256.414 266.259 258.891 265.707 261.398 266.589C263.904 267.471 265.49 269.453 264.94 271.015C264.39 272.577 261.913 273.129 259.406 272.247C256.9 271.365 255.314 269.383 255.864 267.821Z\"\n        fill=\"#F3C553\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M303.524 364.707L298.821 340.323C298.821 340.323 297.026 330.407 288.965 332.548C282.844 334.174 281.902 339.469 282.906 344.122C284.024 349.301 287.973 367.878 287.973 367.878C287.973 367.878 277.528 361.013 273.485 360.327C269.782 359.698 265.162 361.817 263.955 364.791C260.769 372.634 273.848 385.827 288.42 388.806C302.992 391.786 317.949 384.286 323.462 371.391C329.598 357.039 324.98 349.892 318.711 350.704C312.443 351.517 304.838 362.288 303.524 364.707Z\"\n        fill=\"#E2BC9B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M301.75 355.507L303.524 364.707C304.838 362.288 312.443 351.517 318.711 350.704C320.924 350.417 322.931 351.122 324.331 352.786C324.018 355.503 323.152 358.706 321.58 362.381C316.068 375.276 301.111 382.776 286.539 379.797C276.912 377.829 267.936 371.402 263.871 365.006C263.898 364.933 263.926 364.861 263.955 364.791C265.162 361.817 269.782 359.698 273.485 360.327C277.528 361.013 287.973 367.878 287.973 367.878C287.973 367.878 287.096 363.748 286.05 358.841L286.092 358.868C286.092 358.868 284.221 350.07 282.691 342.896C282.105 338.575 283.376 334.033 288.965 332.548C294.86 330.982 297.404 335.865 298.345 338.602L301.643 355.698C301.675 355.639 301.71 355.575 301.75 355.507Z\"\n        fill=\"#ECCFB5\"\n      />\n      <path\n        d=\"M303.524 364.707L298.821 340.323C298.821 340.323 297.026 330.407 288.965 332.548C282.844 334.174 281.902 339.469 282.906 344.122C284.024 349.301 287.973 367.878 287.973 367.878C287.973 367.878 277.528 361.013 273.485 360.327C269.782 359.698 265.162 361.817 263.955 364.791C260.769 372.634 273.848 385.827 288.42 388.806C302.992 391.786 317.949 384.286 323.462 371.391C329.598 357.039 324.98 349.892 318.711 350.704C312.443 351.517 304.838 362.288 303.524 364.707Z\"\n        stroke=\"#9A7451\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M312.686 282.133C321.415 281.993 328.63 290.547 328.801 301.239C328.972 311.93 322.033 320.71 313.304 320.85C304.575 320.989 297.36 312.435 297.189 301.744C297.018 291.052 303.956 282.272 312.686 282.133Z\"\n        fill=\"#6A895B\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M311.267 282.232C316.725 285.499 320.548 292.353 320.675 300.328C320.825 309.683 315.842 317.659 308.925 320.17C302.285 317.953 297.329 310.574 297.188 301.744C297.027 291.645 303.22 283.241 311.267 282.232Z\"\n        fill=\"#5C784D\"\n      />\n      <path\n        d=\"M312.686 282.133C321.415 281.993 328.63 290.547 328.801 301.239C328.972 311.93 322.033 320.71 313.304 320.85C304.575 320.989 297.36 312.435 297.189 301.744C297.018 291.052 303.956 282.272 312.686 282.133Z\"\n        stroke=\"#374536\"\n        strokeWidth=\"6\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M312.728 261.097C317.701 262.218 322.642 259.095 323.763 254.122C324.054 252.834 324.352 251.508 324.642 250.219C325.764 245.246 322.641 240.306 317.668 239.184C311.287 237.746 303.856 236.071 303.856 236.071L298.917 257.983C298.917 257.983 306.347 259.658 312.728 261.097Z\"\n        fill=\"#F5DA70\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M302.136 243.702L315.948 246.815C320.282 247.792 323.211 251.671 323.149 255.943C321.371 259.828 317.063 262.074 312.728 261.097C306.347 259.658 298.917 257.983 298.917 257.983L302.136 243.702Z\"\n        fill=\"#EFC415\"\n      />\n      <path\n        d=\"M312.728 261.097C317.701 262.218 322.642 259.095 323.763 254.122C324.054 252.834 324.352 251.508 324.642 250.219C325.764 245.246 322.641 240.306 317.668 239.184C311.287 237.746 303.856 236.071 303.856 236.071L298.917 257.983C298.917 257.983 306.347 259.658 312.728 261.097Z\"\n        stroke=\"#C19607\"\n        strokeWidth=\"5.25\"\n        strokeMiterlimit=\"10\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  )\n}\n\nfunction Arrow({ direction }: { direction: 'up' | 'down' }): JSX.Element {\n  return (\n    <svg\n      width=\"120\"\n      height=\"173\"\n      viewBox=\"0 0 232 335\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={\n        direction === 'down'\n          ? 'rotate-180 transition-transform duration-150'\n          : 'transition-transform duration-150'\n      }\n      role=\"img\"\n      aria-label={`Arrow pointing ${direction}`}\n      style={{\n        viewTransitionName: 'arrow-direction',\n      }}\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M229.212 157.623C232.34 162.312 232.632 168.343 229.973 173.313C227.314 178.284 222.135 181.387 216.498 181.387H171.177V318.848C171.177 322.901 169.567 326.788 166.701 329.654C163.834 332.521 159.947 334.131 155.894 334.131H75.8861C71.8331 334.131 67.9461 332.521 65.0801 329.654C62.2141 326.788 60.6041 322.901 60.6041 318.848V181.387H15.2831C9.64615 181.387 4.46615 178.284 1.80715 173.313C-0.851852 168.343 -0.558859 162.312 2.56914 157.623C28.4951 118.757 79.4511 42.369 103.176 6.80196C106.011 2.55196 110.782 -3.05176e-05 115.89 -3.05176e-05C120.999 -3.05176e-05 125.769 2.55196 128.604 6.80196C152.329 42.369 203.285 118.757 229.212 157.623Z\"\n        fill=\"white\"\n      />\n    </svg>\n  )\n}\n\nexport default function Spinner({\n  direction,\n}: {\n  direction: 'up' | 'down'\n}): JSX.Element {\n  const isRotating = useRotatingSpinner()\n  return (\n    <div className=\"relative w-[300px] h-[300px]\">\n      <Pizza isRotating={isRotating} />\n      <div className=\"absolute inset-0 flex items-center justify-center\">\n        <Arrow direction={direction} />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/StartButton.tsx",
    "content": "import React from 'react'\n\nexport default function StartButton({\n  onClick,\n}: {\n  onClick: React.MouseEventHandler<HTMLButtonElement>\n}): React.ReactElement {\n  return (\n    <button\n      id=\"start-button\"\n      onClick={onClick}\n      className=\"px-4 py-2 bg-linear-to-b from-green-500 to-green-600 text-white rounded-md hover:from-green-500 hover:to-green-700 transition-all duration-200 border border-green-600 shadow-sm hover:shadow-md text-shadow\"\n    >\n      Start\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/StopButton.tsx",
    "content": "import React from 'react'\n\nexport default function StopButton({\n  isDownloading,\n  onClick,\n}: {\n  onClick: React.MouseEventHandler<HTMLButtonElement>\n  isDownloading?: boolean\n}): React.ReactElement {\n  return (\n    <button\n      className=\"px-2 py-1 text-xs text-orange-500 dark:text-orange-400 bg-transparent hover:bg-orange-100 dark:hover:bg-orange-900 rounded transition-colors duration-200 flex items-center\"\n      onClick={onClick}\n    >\n      <svg\n        className=\"w-4 h-4 mr-1\"\n        viewBox=\"0 0 24 24\"\n        fill=\"currentColor\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" />\n      </svg>\n      {isDownloading ? 'Stop Download' : 'Stop Upload'}\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/SubtitleText.tsx",
    "content": "import React, { JSX } from 'react'\n\nexport default function SubtitleText({\n  children,\n}: {\n  children: React.ReactNode\n}): JSX.Element {\n  return (\n    <p className=\"text-sm text-center text-stone-600 dark:text-stone-400 max-w-md\">\n      {children}\n    </p>\n  )\n}\n"
  },
  {
    "path": "src/components/TermsAcceptance.tsx",
    "content": "'use client'\n\nimport { JSX, useState } from 'react'\nimport CancelButton from './CancelButton'\n\nexport default function TermsAcceptance(): JSX.Element {\n  const [showModal, setShowModal] = useState(false)\n\n  return (\n    <>\n      <div className=\"flex justify-center\">\n        <span className=\"text-xs text-stone-600 dark:text-stone-400\">\n          By selecting a file, you agree to{' '}\n          <button\n            onClick={() => setShowModal(true)}\n            className=\"underline hover:text-stone-900 dark:hover:text-stone-200 transition-colors duration-200\"\n            aria-label=\"View upload terms\"\n          >\n            our terms\n          </button>\n          .\n        </span>\n      </div>\n\n      {showModal && (\n        <div\n          className=\"fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50\"\n          role=\"dialog\"\n          aria-modal=\"true\"\n          aria-labelledby=\"modal-title\"\n          onClick={() => setShowModal(false)}\n        >\n          <div\n            className=\"bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-700 rounded-lg p-8 max-w-md w-full shadow-lg\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            <h2\n              id=\"modal-title\"\n              className=\"text-xl font-bold mb-4 text-stone-900 dark:text-stone-50\"\n            >\n              FilePizza Terms\n            </h2>\n\n            <div className=\"space-y-4 text-stone-700 dark:text-stone-300\">\n              <ul className=\"list-none space-y-3\">\n                <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n                  <span className=\"text-base\">📤</span>\n                  <span className=\"text-sm\">\n                    Files are shared directly between browsers — no server\n                    storage\n                  </span>\n                </li>\n                <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n                  <span className=\"text-base\">✅</span>\n                  <span className=\"text-sm\">\n                    Only upload files you have the right to share\n                  </span>\n                </li>\n                <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n                  <span className=\"text-base\">🔒</span>\n                  <span className=\"text-sm\">\n                    Share download links only with known recipients\n                  </span>\n                </li>\n                <li className=\"flex items-start gap-3 px-4 py-2 rounded-lg bg-stone-100 dark:bg-stone-800\">\n                  <span className=\"text-base\">⚠️</span>\n                  <span className=\"text-sm\">\n                    No illegal or harmful content allowed\n                  </span>\n                </li>\n              </ul>\n\n              <p className=\"text-sm italic\">\n                By uploading a file, you confirm that you understand and agree\n                to these terms.\n              </p>\n            </div>\n\n            <div className=\"mt-6 flex justify-end\">\n              <CancelButton\n                text=\"Got it!\"\n                onClick={() => setShowModal(false)}\n              />\n            </div>\n          </div>\n        </div>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/ThemeProvider.tsx",
    "content": "'use client'\n\nimport * as React from 'react'\nimport { ThemeProvider as NextThemesProvider } from 'next-themes'\n\nexport type ThemeProviderProps = Parameters<typeof NextThemesProvider>[0]\nexport const ThemeProvider = NextThemesProvider as React.FC<ThemeProviderProps>\n"
  },
  {
    "path": "src/components/TitleText.tsx",
    "content": "import React, { JSX } from 'react'\n\nexport default function TitleText({\n  children,\n}: {\n  children: React.ReactNode\n}): JSX.Element {\n  return (\n    <p className=\"text-lg text-center text-stone-800 dark:text-stone-200 max-w-md\">\n      {children}\n    </p>\n  )\n}\n"
  },
  {
    "path": "src/components/TypeBadge.tsx",
    "content": "import React, { JSX } from 'react'\n\nfunction getTypeColor(fileType: string): string {\n  if (fileType.startsWith('image/'))\n    return 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'\n  if (fileType.startsWith('text/'))\n    return 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'\n  if (fileType.startsWith('audio/'))\n    return 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200'\n  if (fileType.startsWith('video/'))\n    return 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200'\n  return 'bg-stone-100 dark:bg-stone-900 text-stone-800 dark:text-stone-200'\n}\n\nexport default function TypeBadge({ type }: { type: string }): JSX.Element {\n  return (\n    <div\n      className={`px-2 py-1 text-[10px] font-semibold rounded ${getTypeColor(\n        type,\n      )} transition-all duration-300`}\n    >\n      {type}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/UnlockButton.tsx",
    "content": "import React, { JSX } from 'react'\n\nexport default function UnlockButton({\n  onClick,\n}: {\n  onClick?: React.MouseEventHandler<HTMLButtonElement>\n}): JSX.Element {\n  return (\n    <button\n      onClick={onClick}\n      className=\"px-4 py-2 bg-linear-to-b from-green-500 to-green-600 text-white rounded-md hover:from-green-500 hover:to-green-700 transition-all duration-200 border border-green-600 shadow-sm hover:shadow-md text-shadow\"\n    >\n      Unlock\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/UploadFileList.tsx",
    "content": "import React, { JSX } from 'react'\nimport TypeBadge from './TypeBadge'\n\ntype UploadedFileLike = {\n  fileName?: string\n  type: string\n}\n\nexport default function UploadFileList({\n  files,\n  onRemove,\n}: {\n  files: UploadedFileLike[]\n  onRemove?: (index: number) => void\n}): JSX.Element {\n  const items = files.map((f: UploadedFileLike, i: number) => (\n    <div\n      key={f.fileName}\n      className={`w-full border-b border-stone-300 dark:border-stone-700 last:border-0`}\n    >\n      <div className=\"flex justify-between items-center py-2 pl-3 pr-2\">\n        <p className=\"truncate text-sm font-medium text-stone-800 dark:text-stone-200\">\n          {f.fileName}\n        </p>\n        <div className=\"flex items-center\">\n          <TypeBadge type={f.type} />\n          {onRemove && (\n            <button\n              onClick={() => onRemove?.(i)}\n              className=\"text-stone-500 hover:text-stone-700 dark:text-stone-400 dark:hover:text-stone-200 focus:outline-none pl-3 pr-1\"\n            >\n              ✕\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  ))\n\n  return (\n    <div className=\"w-full border border-stone-300 dark:border-stone-700 rounded-md shadow-sm dark:shadow-sm-dark bg-white dark:bg-stone-800\">\n      {items}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Uploader.tsx",
    "content": "'use client'\n\nimport React, { JSX, useCallback, useEffect } from 'react'\nimport { UploadedFile, UploaderConnectionStatus } from '../types'\nimport { useWebRTCPeer } from './WebRTCProvider'\nimport QRCode from 'react-qr-code'\nimport Loading from './Loading'\nimport StopButton from './StopButton'\nimport { useUploaderChannel } from '../hooks/useUploaderChannel'\nimport { useUploaderConnections } from '../hooks/useUploaderConnections'\nimport { CopyableInput } from './CopyableInput'\nimport { ConnectionListItem } from './ConnectionListItem'\nimport { ErrorMessage } from './ErrorMessage'\nimport { setRotating } from '../hooks/useRotatingSpinner'\n\nconst QR_CODE_SIZE = 128\n\nexport default function Uploader({\n  files,\n  password,\n  onStop,\n}: {\n  files: UploadedFile[]\n  password: string\n  onStop: () => void\n}): JSX.Element {\n  const { peer, stop } = useWebRTCPeer()\n  const { isLoading, error, longSlug, shortSlug, longURL, shortURL } =\n    useUploaderChannel(peer.id)\n  const connections = useUploaderConnections(peer, files, password)\n\n  const handleStop = useCallback(() => {\n    stop()\n    onStop()\n  }, [stop, onStop])\n\n  const activeDownloaders = connections.filter(\n    (conn) => conn.status === UploaderConnectionStatus.Uploading,\n  ).length\n\n  useEffect(() => {\n    setRotating(activeDownloaders > 0)\n  }, [activeDownloaders])\n\n  if (isLoading || !longSlug || !shortSlug) {\n    return <Loading text=\"Creating channel...\" />\n  }\n\n  if (error) {\n    return <ErrorMessage message={error.message} />\n  }\n\n  return (\n    <>\n      <div className=\"flex w-full items-center\">\n        <div className=\"flex-none mr-4 bg-white p-2\">\n          <QRCode value={shortURL ?? ''} size={QR_CODE_SIZE} />\n        </div>\n        <div className=\"flex-auto flex flex-col justify-center space-y-2\">\n          <CopyableInput label=\"Long URL\" value={longURL ?? ''} />\n          <CopyableInput label=\"Short URL\" value={shortURL ?? ''} />\n        </div>\n      </div>\n      <div className=\"mt-6 pt-4 border-t border-stone-200 dark:border-stone-700 w-full\">\n        <div className=\"flex justify-between items-center mb-2\">\n          <h2 className=\"text-lg font-semibold text-stone-400 dark:text-stone-200\">\n            {activeDownloaders} Downloading, {connections.length} Total\n          </h2>\n          <StopButton onClick={handleStop} />\n        </div>\n        {connections.map((conn, i) => (\n          <ConnectionListItem key={i} conn={conn} />\n        ))}\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/WebRTCProvider.tsx",
    "content": "'use client'\n\nimport React, {\n  JSX,\n  useState,\n  useEffect,\n  useContext,\n  useCallback,\n  useMemo,\n} from 'react'\nimport Loading from './Loading'\nimport Peer from 'peerjs'\nimport { ErrorMessage } from './ErrorMessage'\n\nexport type WebRTCPeerValue = {\n  peer: Peer\n  stop: () => void\n}\n\nconst WebRTCContext = React.createContext<WebRTCPeerValue | null>(null)\n\nexport const useWebRTCPeer = (): WebRTCPeerValue => {\n  const value = useContext(WebRTCContext)\n  if (value === null) {\n    throw new Error('useWebRTC must be used within a WebRTCProvider')\n  }\n  return value\n}\n\nlet globalPeer: Peer | null = null\n\nasync function getOrCreateGlobalPeer(): Promise<Peer> {\n  if (!globalPeer) {\n    const response = await fetch('/api/ice', {\n      method: 'POST',\n    })\n    const { host, path, iceServers } = await response.json()\n    console.log('[WebRTCProvider] ICE servers:', iceServers)\n    console.log('[WebRTCProvider] host:', host)\n    console.log('[WebRTCProvider] path:', path)\n\n    globalPeer = new Peer({\n      debug: 3,\n      host,\n      path,\n      config: {\n        iceServers,\n      },\n    })\n  }\n\n  if (globalPeer.id) {\n    return globalPeer\n  }\n\n  await new Promise<void>((resolve) => {\n    const listener = (id: string) => {\n      console.log('[WebRTCProvider] Peer ID:', id)\n      globalPeer?.off('open', listener)\n      resolve()\n    }\n    globalPeer?.on('open', listener)\n  })\n\n  return globalPeer\n}\n\nexport default function WebRTCPeerProvider({\n  children,\n}: {\n  children?: React.ReactNode\n}): JSX.Element {\n  const [peerValue, setPeerValue] = useState<Peer | null>(globalPeer)\n  const [isStopped, setIsStopped] = useState(false)\n  const [error, setError] = useState<Error | null>(null)\n\n  const stop = useCallback(() => {\n    console.log('[WebRTCProvider] Stopping peer')\n    globalPeer?.destroy()\n    globalPeer = null\n    setPeerValue(null)\n    setIsStopped(true)\n  }, [])\n\n  useEffect(() => {\n    getOrCreateGlobalPeer().then(setPeerValue).catch(setError)\n  }, [])\n\n  const value = useMemo(() => ({ peer: peerValue!, stop }), [peerValue, stop])\n\n  if (error) {\n    return <ErrorMessage message={error.message} />\n  }\n\n  if (isStopped) {\n    return <></>\n  }\n\n  if (!peerValue) {\n    return <Loading text=\"Initializing WebRTC peer...\" />\n  }\n\n  return (\n    <WebRTCContext.Provider value={value}>{children}</WebRTCContext.Provider>\n  )\n}\n"
  },
  {
    "path": "src/components/Wordmark.tsx",
    "content": "import { JSX } from 'react'\n\nexport default function Wordmark(): JSX.Element {\n  return (\n    <svg\n      width=\"972\"\n      height=\"212\"\n      viewBox=\"0 0 972 212\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className=\"h-12 w-auto text-red-600 dark:brightness-0 dark:invert\"\n      aria-label=\"FilePizza logo\"\n      role=\"img\"\n    >\n      <path\n        d=\"M870.506 211.68C859.866 211.68 851 208.04 843.906 200.76C836.813 193.48 833.266 182.093 833.266 166.6C833.266 152.787 835.973 138.32 841.386 123.2C846.986 107.893 855.2 95.0133 866.026 84.56C877.04 73.92 890.106 68.6 905.226 68.6C912.88 68.6 918.573 69.9066 922.306 72.52C926.04 75.1333 927.906 78.5866 927.906 82.88V84.84L930.986 70H971.306L951.146 165.2C950.4 168 950.026 170.987 950.026 174.16C950.026 177.893 950.866 180.6 952.546 182.28C954.413 183.773 957.4 184.52 961.506 184.52C964.12 184.52 966.173 184.147 967.666 183.4C963.56 193.853 959.64 201.227 955.906 205.52C952.173 209.627 946.76 211.68 939.666 211.68C932.013 211.68 925.76 209.44 920.906 204.96C916.24 200.293 913.346 193.853 912.226 185.64C900.84 203 886.933 211.68 870.506 211.68ZM888.706 184.52C893.373 184.52 897.946 182.373 902.426 178.08C907.093 173.6 910.266 167.533 911.946 159.88L925.386 96.6C925.386 94.1733 924.453 91.84 922.586 89.6C920.72 87.1733 917.826 85.96 913.906 85.96C906.44 85.96 899.72 90.3466 893.746 99.12C887.773 107.707 883.106 118.16 879.746 130.48C876.386 142.613 874.706 153.347 874.706 162.68C874.706 172.013 876.013 177.987 878.626 180.6C881.426 183.213 884.786 184.52 888.706 184.52Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M788.541 85.12H772.861C767.634 85.12 763.714 85.4 761.101 85.96C758.674 86.52 757.461 87.7333 757.461 89.6C757.461 89.9733 757.927 90.44 758.861 91C759.981 91.3733 760.541 92.96 760.541 95.76C760.541 100.613 759.047 104.347 756.061 106.96C753.074 109.387 749.527 110.6 745.421 110.6C741.874 110.6 738.794 109.573 736.181 107.52C733.754 105.28 732.541 102.2 732.541 98.28C732.541 94.36 733.847 90.16 736.461 85.68C739.261 81.2 743.087 77.4667 747.941 74.48C752.981 71.4933 758.674 70 765.021 70H836.981L768.381 188.44C769.501 188.44 771.554 188.627 774.541 189C780.887 189.747 785.927 190.12 789.661 190.12C797.687 190.12 802.074 188.253 802.821 184.52C801.141 184.333 799.834 183.773 798.901 182.84C797.967 181.72 797.501 180.32 797.501 178.64C797.501 175.653 798.901 172.947 801.701 170.52C804.501 167.907 808.327 166.6 813.181 166.6C817.287 166.6 820.367 167.813 822.421 170.24C824.474 172.667 825.501 175.933 825.501 180.04C825.501 184.52 824.101 189.093 821.301 193.76C818.501 198.427 814.487 202.347 809.261 205.52C804.034 208.507 798.061 210 791.341 210H717.141L788.541 85.12Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M585.619 54.88C579.459 54.88 574.233 52.7333 569.939 48.44C565.646 44.1467 563.499 38.92 563.499 32.76C563.499 26.6 565.646 21.3733 569.939 17.08C574.233 12.6 579.459 10.36 585.619 10.36C591.779 10.36 597.006 12.6 601.299 17.08C605.779 21.3733 608.019 26.6 608.019 32.76C608.019 38.92 605.779 44.1467 601.299 48.44C597.006 52.7333 591.779 54.88 585.619 54.88ZM566.579 211.68C557.619 211.68 550.339 208.88 544.739 203.28C539.326 197.68 536.619 189.28 536.619 178.08C536.619 173.413 537.366 167.347 538.859 159.88L557.899 70H598.219L578.059 165.2C577.313 168 576.939 170.987 576.939 174.16C576.939 177.893 577.779 180.6 579.459 182.28C581.326 183.773 584.313 184.52 588.419 184.52C591.779 184.52 594.766 183.96 597.379 182.84C596.633 192.173 593.273 199.36 587.299 204.4C581.513 209.253 574.606 211.68 566.579 211.68ZM668.779 85.12H653.099C647.873 85.12 643.953 85.4 641.339 85.96C638.913 86.52 637.699 87.7333 637.699 89.6C637.699 89.9733 638.166 90.44 639.099 91C640.219 91.3733 640.779 92.96 640.779 95.76C640.779 100.613 639.286 104.347 636.299 106.96C633.313 109.387 629.766 110.6 625.659 110.6C622.113 110.6 619.033 109.573 616.419 107.52C613.993 105.28 612.779 102.2 612.779 98.28C612.779 94.36 614.086 90.16 616.699 85.68C619.499 81.2 623.326 77.4667 628.179 74.48C633.219 71.4933 638.913 70 645.259 70H717.219L648.619 188.44C649.739 188.44 651.793 188.627 654.779 189C661.126 189.747 666.166 190.12 669.899 190.12C677.926 190.12 682.313 188.253 683.059 184.52C681.379 184.333 680.073 183.773 679.139 182.84C678.206 181.72 677.739 180.32 677.739 178.64C677.739 175.653 679.139 172.947 681.939 170.52C684.739 167.907 688.566 166.6 693.419 166.6C697.526 166.6 700.606 167.813 702.659 170.24C704.713 172.667 705.739 175.933 705.739 180.04C705.739 184.52 704.339 189.093 701.539 193.76C698.739 198.427 694.726 202.347 689.499 205.52C684.273 208.507 678.299 210 671.579 210H597.379L668.779 85.12Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M442.752 31.92L484.192 26.32L464.312 119.84C473.272 119.093 481.485 115.173 488.952 108.08C496.605 100.987 502.578 92.2133 506.872 81.76C511.165 71.3067 513.312 61.04 513.312 50.96C513.312 39.76 510.418 30.6133 504.632 23.52C498.845 16.4267 490.165 12.88 478.592 12.88C455.818 12.88 437.992 18.8533 425.112 30.8C412.418 42.7467 406.072 59.4533 406.072 80.92C406.072 87.8267 406.725 92.68 408.032 95.48C409.338 98.0933 409.992 99.5867 409.992 99.96C399.912 99.96 392.352 97.9067 387.312 93.8C382.458 89.5067 380.032 82.5067 380.032 72.8C380.032 60.8533 384.885 49.28 394.592 38.08C404.485 26.6933 417.085 17.5467 432.392 10.64C447.698 3.54667 463.005 0 478.312 0C493.058 0 505.378 2.52 515.272 7.56C525.165 12.6 532.445 19.32 537.112 27.72C541.965 35.9333 544.392 45.08 544.392 55.16C544.392 67.2933 541.032 79.1467 534.312 90.72C527.778 102.293 518.352 111.813 506.032 119.28C493.712 126.56 479.525 130.2 463.472 130.2H462.072L444.992 210H404.672L442.752 31.92Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M315.293 211.68C301.666 211.68 291.026 208.133 283.372 201.04C275.719 193.76 271.892 182.467 271.892 167.16C271.892 154.28 274.413 140.093 279.453 124.6C284.493 109.107 292.706 95.76 304.092 84.56C315.479 73.1733 329.946 67.48 347.492 67.48C368.026 67.48 378.293 76.44 378.293 94.36C378.293 104.813 375.306 114.427 369.333 123.2C363.359 131.973 355.426 139.067 345.533 144.48C335.639 149.707 325.093 152.693 313.893 153.44C313.519 157.547 313.332 160.347 313.332 161.84C313.332 177.333 319.119 185.08 330.693 185.08C335.919 185.08 341.519 183.68 347.492 180.88C353.466 178.08 358.879 174.533 363.733 170.24C358.693 197.867 342.546 211.68 315.293 211.68ZM315.853 140C322.946 139.813 329.573 137.48 335.733 133C342.079 128.52 347.119 122.827 350.853 115.92C354.773 108.827 356.733 101.453 356.733 93.8C356.733 86.1466 354.399 82.32 349.733 82.32C343.199 82.32 336.666 88.2933 330.133 100.24C323.786 112 319.026 125.253 315.853 140Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M228.884 211.68C219.924 211.68 212.644 208.88 207.044 203.28C201.631 197.68 198.924 189.28 198.924 178.08C198.924 173.413 199.671 167.347 201.164 159.88L231.124 19.6L272.564 14L240.364 165.2C239.617 168 239.244 170.987 239.244 174.16C239.244 177.893 240.084 180.6 241.764 182.28C243.631 183.773 246.617 184.52 250.724 184.52C256.137 184.52 261.177 182.28 265.844 177.8C270.511 173.133 273.871 167.16 275.924 159.88H287.684C280.777 180.04 271.911 193.76 261.084 201.04C250.257 208.133 239.524 211.68 228.884 211.68Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M68.56 22.96H65.48C53.16 22.96 43.5467 27.2533 36.64 35.84C29.92 44.24 26.56 59.2667 26.56 80.92C26.56 87.8267 27.2133 92.68 28.52 95.48C29.8267 98.0933 30.48 99.5867 30.48 99.96C20.4 99.96 12.84 97.9067 7.79999 93.8C2.94666 89.5067 0.519989 82.5067 0.519989 72.8C0.519989 59.5467 3.78666 47.5067 10.32 36.68C16.8533 25.6667 27.2133 16.8 41.4 10.08C55.7733 3.36 73.9733 0 96 0C102.72 0 111.96 0.84 123.72 2.52C124.84 2.70668 129.227 3.26668 136.88 4.20001C144.72 5.13334 152.653 5.60001 160.68 5.60001C174.68 5.60001 188.213 3.82668 201.28 0.28001C199.04 12.6 195.213 22.96 189.8 31.36C184.387 39.5733 176.64 43.68 166.56 43.68C159.84 43.68 153.867 43.0267 148.64 41.72C143.413 40.2267 137.16 38.08 129.88 35.28C122.413 32.48 115.04 30.0533 107.76 28L95.72 84H131L126.52 104.72H91.24L68.56 210H28.24L68.56 22.96ZM154.24 211.68C145.28 211.68 138 208.88 132.4 203.28C126.987 197.68 124.28 189.28 124.28 178.08C124.28 173.413 125.027 167.347 126.52 159.88L145.56 70H185.88L165.72 165.2C164.973 168 164.6 170.987 164.6 174.16C164.6 177.893 165.44 180.6 167.12 182.28C168.987 183.773 171.973 184.52 176.08 184.52C181.493 184.52 186.533 182.28 191.2 177.8C195.867 173.133 199.227 167.16 201.28 159.88H213.04C206.133 180.04 197.267 193.76 186.44 201.04C175.613 208.133 164.88 211.68 154.24 211.68Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "src/config.ts",
    "content": "import toppings from './toppings'\n\nexport default {\n  redisURL: 'redis://localhost:6379/0',\n  channel: {\n    ttl: 60 * 60, // 1 hour\n  },\n  bodyKeys: {\n    uploaderPeerID: {\n      min: 3,\n      max: 256,\n    },\n    slug: {\n      min: 3,\n      max: 256,\n    },\n  },\n  shortSlug: {\n    numChars: 8,\n    chars: '0123456789abcdefghijklmnopqrstuvwxyz',\n    maxAttempts: 8,\n  },\n  longSlug: {\n    numWords: 4,\n    words: toppings,\n    maxAttempts: 8,\n  },\n}\n"
  },
  {
    "path": "src/coturn.ts",
    "content": "import crypto from 'crypto'\nimport { getRedisClient } from './redisClient'\n\nfunction generateHMACKey(\n  username: string,\n  realm: string,\n  password: string,\n): string {\n  const str = `${username}:${realm}:${password}`\n  return crypto.createHash('md5').update(str).digest('hex')\n}\n\nexport async function setTurnCredentials(\n  username: string,\n  password: string,\n  ttl: number,\n): Promise<void> {\n  if (!process.env.COTURN_ENABLED) {\n    return\n  }\n\n  const realm = process.env.TURN_REALM || 'file.pizza'\n\n  if (!realm) {\n    throw new Error('TURN_REALM environment variable not set')\n  }\n\n  const redis = getRedisClient()\n\n  const hmacKey = generateHMACKey(username, realm, password)\n  const key = `turn/realm/${realm}/user/${username}/key`\n\n  await redis.setex(key, ttl, hmacKey)\n}\n"
  },
  {
    "path": "src/fs.ts",
    "content": "import { UploadedFile } from './types'\n\nconst getAsFile = (entry: any): Promise<File> =>\n  new Promise((resolve, reject) => {\n    entry.file((file: UploadedFile) => {\n      file.entryFullPath = entry.fullPath\n      resolve(file)\n    }, reject)\n  })\n\nconst readDirectoryEntries = (reader: any): Promise<any[]> =>\n  new Promise((resolve, reject) => {\n    reader.readEntries((entries) => {\n      resolve(entries)\n    }, reject)\n  })\n\nconst scanDirectoryEntry = async (entry: any): Promise<File[]> => {\n  const directoryReader = entry.createReader()\n  const result: File[] = []\n  // eslint-disable-next-line no-constant-condition\n  while (true) {\n    const subentries = await readDirectoryEntries(directoryReader)\n    if (!subentries.length) {\n      return result\n    }\n\n    for (const se of subentries) {\n      if (se.isDirectory) {\n        const ses = await scanDirectoryEntry(se)\n        result.push(...ses)\n      } else {\n        const file = await getAsFile(se)\n        result.push(file)\n      }\n    }\n  }\n}\n\nexport const extractFileList = async (\n  e: React.DragEvent | DragEvent,\n): Promise<File[]> => {\n  if (!e.dataTransfer || !e.dataTransfer.items.length) {\n    return []\n  }\n\n  const items = e.dataTransfer.items\n  const scans: Promise<File[]>[] = []\n  const files: Promise<File>[] = []\n\n  for (let i = 0; i < items.length; i++) {\n    const item = items[i]\n    const entry = item.webkitGetAsEntry()\n    if (entry) {\n      if (entry.isDirectory) {\n        scans.push(scanDirectoryEntry(entry))\n      } else {\n        files.push(getAsFile(entry))\n      }\n    }\n  }\n\n  const scanResults = await Promise.all(scans)\n  const fileResults = await Promise.all(files)\n\n  return scanResults.flat().concat(fileResults)\n}\n\n// Borrowed from StackOverflow\n// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript\nexport const formatSize = (bytes: number): string => {\n  if (bytes === 0) {\n    return '0 Bytes'\n  }\n  const k = 1000\n  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n  const i = Math.floor(Math.log(bytes) / Math.log(k))\n  return `${(bytes / Math.pow(k, i)).toPrecision(3)} ${sizes[i]}`\n}\n\nexport const getFileName = (file: UploadedFile): string => {\n  return file.name ?? file.entryFullPath ?? ''\n}\n"
  },
  {
    "path": "src/hooks/useClipboard.ts",
    "content": "import { useState, useCallback, useEffect } from 'react'\n\nexport default function useClipboard(\n  text: string,\n  delay = 1000,\n): {\n  hasCopied: boolean\n  onCopy: () => void\n} {\n  const [hasCopied, setHasCopied] = useState(false)\n\n  const onCopy = useCallback(() => {\n    if (navigator.clipboard && navigator.clipboard.writeText) {\n      navigator.clipboard\n        .writeText(text)\n        .then(() => {\n          setHasCopied(true)\n        })\n        .catch((error) => {\n          console.error('Clipboard API error:', error)\n          fallbackCopyText(text)\n        })\n    } else {\n      fallbackCopyText(text)\n    }\n  }, [text])\n\n  const fallbackCopyText = (textToCopy: string) => {\n    const textArea = document.createElement('textarea')\n    textArea.value = textToCopy\n\n    textArea.style.position = 'absolute'\n    textArea.style.left = '-999999px'\n\n    document.body.appendChild(textArea)\n    textArea.select()\n\n    try {\n      document.execCommand('copy')\n      setHasCopied(true)\n    } catch (error) {\n      console.error('execCommand:', error)\n    } finally {\n      textArea.remove()\n    }\n  }\n\n  useEffect(() => {\n    let timeoutId: NodeJS.Timeout\n    if (hasCopied) {\n      timeoutId = setTimeout(() => {\n        setHasCopied(false)\n      }, delay)\n    }\n    return () => {\n      clearTimeout(timeoutId)\n    }\n  }, [hasCopied, delay])\n\n  return { hasCopied, onCopy }\n}\n"
  },
  {
    "path": "src/hooks/useDownloader.ts",
    "content": "import { useState, useCallback, useRef, useEffect } from 'react'\nimport { useWebRTCPeer } from '../components/WebRTCProvider'\nimport { z } from 'zod'\nimport { ChunkMessage, decodeMessage, Message, MessageType } from '../messages'\nimport { DataConnection } from 'peerjs'\nimport {\n  streamDownloadSingleFile,\n  streamDownloadMultipleFiles,\n} from '../utils/download'\nimport {\n  browserName,\n  browserVersion,\n  osName,\n  osVersion,\n  mobileVendor,\n  mobileModel,\n} from 'react-device-detect'\nimport { setRotating } from './useRotatingSpinner'\nconst cleanErrorMessage = (errorMessage: string): string =>\n  errorMessage.startsWith('Could not connect to peer')\n    ? 'Could not connect to the uploader. Did they close their browser?'\n    : errorMessage\n\nconst getZipFilename = (): string => `filepizza-download-${Date.now()}.zip`\n\nexport function useDownloader(uploaderPeerID: string): {\n  filesInfo: Array<{ fileName: string; size: number; type: string }> | null\n  isConnected: boolean\n  isPasswordRequired: boolean\n  isDownloading: boolean\n  isDone: boolean\n  errorMessage: string | null\n  submitPassword: (password: string) => void\n  startDownload: () => void\n  stopDownload: () => void\n  totalSize: number\n  bytesDownloaded: number\n} {\n  const { peer } = useWebRTCPeer()\n  const [dataConnection, setDataConnection] = useState<DataConnection | null>(\n    null,\n  )\n  const [filesInfo, setFilesInfo] = useState<Array<{\n    fileName: string\n    size: number\n    type: string\n  }> | null>(null)\n  const processChunk = useRef<\n    ((message: z.infer<typeof ChunkMessage>) => void) | null\n  >(null)\n  const [isConnected, setIsConnected] = useState(false)\n  const [isPasswordRequired, setIsPasswordRequired] = useState(false)\n  const [isDownloading, setIsDownloading] = useState(false)\n  const [isDone, setDone] = useState(false)\n  const [bytesDownloaded, setBytesDownloaded] = useState(0)\n  const [errorMessage, setErrorMessage] = useState<string | null>(null)\n\n  useEffect(() => {\n    if (!peer) return\n    console.log('[Downloader] connecting to uploader', uploaderPeerID)\n    const conn = peer.connect(uploaderPeerID, { reliable: true })\n    setDataConnection(conn)\n\n    const handleOpen = () => {\n      console.log('[Downloader] connection opened')\n      setIsConnected(true)\n      conn.send({\n        type: MessageType.RequestInfo,\n        browserName,\n        browserVersion,\n        osName,\n        osVersion,\n        mobileVendor,\n        mobileModel,\n      } as z.infer<typeof Message>)\n    }\n\n    const handleData = (data: unknown) => {\n      try {\n        const message = decodeMessage(data)\n        console.log('[Downloader] received message', message.type)\n        switch (message.type) {\n          case MessageType.PasswordRequired:\n            setIsPasswordRequired(true)\n            if (message.errorMessage) setErrorMessage(message.errorMessage)\n            break\n          case MessageType.Info:\n            setFilesInfo(message.files)\n            setIsPasswordRequired(false)\n            break\n          case MessageType.Chunk:\n            processChunk.current?.(message)\n            setRotating(true)\n            break\n          case MessageType.Error:\n            console.error('[Downloader] received error message:', message.error)\n            setErrorMessage(message.error)\n            conn.close()\n            break\n          case MessageType.Report:\n            console.log('[Downloader] received report message, redirecting')\n            window.location.href = '/reported'\n            break\n        }\n      } catch (err) {\n        console.error('[Downloader] error handling message:', err)\n      }\n    }\n\n    const handleClose = () => {\n      console.log('[Downloader] connection closed')\n      setRotating(false)\n      setDataConnection(null)\n      setIsConnected(false)\n      setIsDownloading(false)\n    }\n\n    const handleError = (err: Error) => {\n      console.error('[Downloader] connection error:', err)\n      setErrorMessage(cleanErrorMessage(err.message))\n      if (conn.open) conn.close()\n      else handleClose()\n    }\n\n    conn.on('open', handleOpen)\n    conn.on('data', handleData)\n    conn.on('error', handleError)\n    conn.on('close', handleClose)\n    peer.on('error', handleError)\n\n    return () => {\n      console.log('[Downloader] cleaning up connection')\n      if (conn.open) {\n        conn.close()\n      } else {\n        conn.once('open', () => {\n          conn.close()\n        })\n      }\n\n      conn.off('open', handleOpen)\n      conn.off('data', handleData)\n      conn.off('error', handleError)\n      conn.off('close', handleClose)\n      peer.off('error', handleError)\n    }\n  }, [peer])\n\n  const submitPassword = useCallback(\n    (pass: string) => {\n      if (!dataConnection) return\n      console.log('[Downloader] submitting password')\n      dataConnection.send({\n        type: MessageType.UsePassword,\n        password: pass,\n      } as z.infer<typeof Message>)\n    },\n    [dataConnection],\n  )\n\n  const startDownload = useCallback(() => {\n    if (!filesInfo || !dataConnection) return\n    console.log('[Downloader] starting download')\n    setIsDownloading(true)\n\n    const fileStreamByPath: Record<\n      string,\n      {\n        stream: ReadableStream<Uint8Array>\n        enqueue: (chunk: Uint8Array) => void\n        close: () => void\n      }\n    > = {}\n    const fileStreams = filesInfo.map((info) => {\n      let enqueue: ((chunk: Uint8Array) => void) | null = null\n      let close: (() => void) | null = null\n      const stream = new ReadableStream<Uint8Array>({\n        start(ctrl) {\n          enqueue = (chunk: Uint8Array) => ctrl.enqueue(chunk)\n          close = () => ctrl.close()\n        },\n      })\n      if (!enqueue || !close)\n        throw new Error('Failed to initialize stream controllers')\n      fileStreamByPath[info.fileName] = { stream, enqueue, close }\n      return stream\n    })\n\n    let nextFileIndex = 0\n    const startNextFileOrFinish = () => {\n      if (nextFileIndex >= filesInfo.length) return\n      console.log(\n        '[Downloader] starting next file:',\n        filesInfo[nextFileIndex].fileName,\n      )\n      dataConnection.send({\n        type: MessageType.Start,\n        fileName: filesInfo[nextFileIndex].fileName,\n        offset: 0,\n      } as z.infer<typeof Message>)\n      nextFileIndex++\n    }\n\n    let chunkCountByFile: Record<string, number> = {}\n    processChunk.current = (message: z.infer<typeof ChunkMessage>) => {\n      const fileStream = fileStreamByPath[message.fileName]\n      if (!fileStream) {\n        console.error('[Downloader] no stream found for', message.fileName)\n        return\n      }\n\n      // Track chunks for e2e testing\n      if (!chunkCountByFile[message.fileName]) {\n        chunkCountByFile[message.fileName] = 0\n      }\n      chunkCountByFile[message.fileName]++\n      console.log(\n        `[Downloader] received chunk ${chunkCountByFile[message.fileName]} for ${message.fileName} (${message.offset}-${message.offset + (message.bytes as ArrayBuffer).byteLength}) final=${message.final}`,\n      )\n\n      const chunkSize = (message.bytes as ArrayBuffer).byteLength\n      setBytesDownloaded((bd) => bd + chunkSize)\n      fileStream.enqueue(new Uint8Array(message.bytes as ArrayBuffer))\n\n      // Send acknowledgment to uploader\n      const ackMessage: Message = {\n        type: MessageType.ChunkAck,\n        fileName: message.fileName,\n        offset: message.offset,\n        bytesReceived: chunkSize,\n      }\n      dataConnection.send(ackMessage)\n      console.log(\n        `[Downloader] sent ack for chunk ${chunkCountByFile[message.fileName]} (${message.offset}, ${chunkSize} bytes)`,\n      )\n\n      if (message.final) {\n        console.log(\n          `[Downloader] finished receiving ${message.fileName} after ${chunkCountByFile[message.fileName]} chunks`,\n        )\n        fileStream.close()\n        startNextFileOrFinish()\n      }\n    }\n\n    const downloads = filesInfo.map((info, i) => ({\n      name: info.fileName.replace(/^\\//, ''),\n      size: info.size,\n      stream: () => fileStreams[i],\n    }))\n\n    const downloadPromise =\n      downloads.length > 1\n        ? streamDownloadMultipleFiles(downloads, getZipFilename())\n        : streamDownloadSingleFile(downloads[0], downloads[0].name)\n\n    downloadPromise\n      .then(() => {\n        console.log('[Downloader] all files downloaded')\n        dataConnection.send({ type: MessageType.Done } as z.infer<\n          typeof Message\n        >)\n        setDone(true)\n      })\n      .catch((err) => console.error('[Downloader] download error:', err))\n\n    startNextFileOrFinish()\n  }, [dataConnection, filesInfo])\n\n  const stopDownload = useCallback(() => {\n    // TODO(@kern): Continue here with stop / pause logic\n    if (dataConnection) {\n      console.log('[Downloader] pausing download')\n      dataConnection.send({ type: MessageType.Pause })\n      dataConnection.close()\n    }\n    setIsDownloading(false)\n    setDone(false)\n    setBytesDownloaded(0)\n    setErrorMessage(null)\n    // fileStreams.forEach((stream) => stream.cancel())\n    // fileStreams.length = 0\n    // Object.values(fileStreamByPath).forEach((stream) => stream.cancel())\n    // Object.keys(fileStreamByPath).forEach((key) => delete fileStreamByPath[key])\n    //   }, [dataConnection, fileStreams, fileStreamByPath])\n  }, [dataConnection])\n\n  return {\n    filesInfo,\n    isConnected,\n    isPasswordRequired,\n    isDownloading,\n    isDone,\n    errorMessage,\n    submitPassword,\n    startDownload,\n    stopDownload,\n    totalSize: filesInfo?.reduce((acc, info) => acc + info.size, 0) ?? 0,\n    bytesDownloaded,\n  }\n}\n"
  },
  {
    "path": "src/hooks/useRotatingSpinner.ts",
    "content": "import { useEffect, useState } from 'react'\n\ntype RotationListener = (isRotating: boolean) => void\n\nlet isRotating = false\nconst listeners = new Set<RotationListener>()\n\nexport function setRotating(rotating: boolean): void {\n  isRotating = rotating\n  notifyListeners()\n}\n\nexport function getRotating(): boolean {\n  return isRotating\n}\n\nexport function addRotationListener(listener: RotationListener): void {\n  listeners.add(listener)\n}\n\nexport function removeRotationListener(listener: RotationListener): void {\n  listeners.delete(listener)\n}\n\nfunction notifyListeners(): void {\n  listeners.forEach((listener) => listener(isRotating))\n}\n\nexport function useRotatingSpinner(): boolean {\n  const [rotating, setRotatingState] = useState(isRotating)\n\n  useEffect(() => {\n    const listener = (newRotating: boolean) => {\n      setRotatingState(newRotating)\n    }\n\n    addRotationListener(listener)\n    return () => removeRotationListener(listener)\n  }, [])\n\n  return rotating\n}\n"
  },
  {
    "path": "src/hooks/useUploaderChannel.ts",
    "content": "import { useQuery, useMutation } from '@tanstack/react-query'\nimport { useEffect } from 'react'\n\nfunction generateURL(slug: string): string {\n  const hostPrefix =\n    window.location.protocol +\n    '//' +\n    window.location.hostname +\n    (window.location.port ? ':' + window.location.port : '')\n  return `${hostPrefix}/download/${slug}`\n}\n\nexport function useUploaderChannel(\n  uploaderPeerID: string,\n  renewInterval = 60_000,\n): {\n  isLoading: boolean\n  error: Error | null\n  longSlug: string | undefined\n  shortSlug: string | undefined\n  longURL: string | undefined\n  shortURL: string | undefined\n} {\n  const { isLoading, error, data } = useQuery({\n    queryKey: ['uploaderChannel', uploaderPeerID],\n    queryFn: async () => {\n      console.log(\n        '[UploaderChannel] creating new channel for peer',\n        uploaderPeerID,\n      )\n      const response = await fetch('/api/create', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ uploaderPeerID }),\n      })\n      if (!response.ok) {\n        console.error(\n          '[UploaderChannel] failed to create channel:',\n          response.status,\n        )\n        throw new Error('Network response was not ok')\n      }\n      const data = await response.json()\n      console.log('[UploaderChannel] channel created successfully:', {\n        longSlug: data.longSlug,\n        shortSlug: data.shortSlug,\n      })\n      return data\n    },\n    refetchOnWindowFocus: false,\n    refetchOnMount: false,\n    refetchOnReconnect: false,\n    staleTime: Infinity,\n  })\n\n  const secret = data?.secret\n  const longSlug = data?.longSlug\n  const shortSlug = data?.shortSlug\n  const longURL = longSlug ? generateURL(longSlug) : undefined\n  const shortURL = shortSlug ? generateURL(shortSlug) : undefined\n\n  const renewMutation = useMutation({\n    mutationFn: async ({ secret: s }: { secret: string }) => {\n      console.log('[UploaderChannel] renewing channel for slug', shortSlug)\n      const response = await fetch('/api/renew', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ slug: shortSlug, secret: s }),\n      })\n      if (!response.ok) {\n        console.error(\n          '[UploaderChannel] failed to renew channel',\n          response.status,\n        )\n        throw new Error('Network response was not ok')\n      }\n      const data = await response.json()\n      console.log('[UploaderChannel] channel renewed successfully')\n      return data\n    },\n  })\n\n  useEffect(() => {\n    if (!secret || !shortSlug) return\n\n    let timeout: NodeJS.Timeout | null = null\n\n    const run = (): void => {\n      timeout = setTimeout(() => {\n        console.log(\n          '[UploaderChannel] scheduling channel renewal in',\n          renewInterval,\n          'ms',\n        )\n        renewMutation.mutate({ secret })\n        run()\n      }, renewInterval)\n    }\n\n    run()\n\n    return () => {\n      if (timeout) {\n        console.log('[UploaderChannel] clearing renewal timeout')\n        clearTimeout(timeout)\n      }\n    }\n  }, [secret, shortSlug, renewMutation, renewInterval])\n\n  useEffect(() => {\n    if (!shortSlug || !secret) return\n\n    const handleUnload = (): void => {\n      console.log('[UploaderChannel] destroying channel on page unload')\n      // Using sendBeacon for best-effort delivery during page unload\n      navigator.sendBeacon('/api/destroy', JSON.stringify({ slug: shortSlug }))\n    }\n\n    window.addEventListener('beforeunload', handleUnload)\n\n    return () => {\n      window.removeEventListener('beforeunload', handleUnload)\n    }\n  }, [shortSlug, secret])\n\n  return {\n    isLoading,\n    error,\n    longSlug,\n    shortSlug,\n    longURL,\n    shortURL,\n  }\n}\n"
  },
  {
    "path": "src/hooks/useUploaderConnections.ts",
    "content": "import { useState, useEffect } from 'react'\nimport Peer, { DataConnection } from 'peerjs'\nimport {\n  UploadedFile,\n  UploaderConnection,\n  UploaderConnectionStatus,\n} from '../types'\nimport {\n  decodeMessage,\n  Message,\n  MessageType,\n  ChunkAckMessage,\n} from '../messages'\nimport { z } from 'zod'\nimport { getFileName } from '../fs'\nimport { setRotating } from './useRotatingSpinner'\n\n// TODO(@kern): Test for better values\nexport const MAX_CHUNK_SIZE = 256 * 1024 // 256 KB\n\nexport function isFinalChunk(offset: number, fileSize: number): boolean {\n  return offset + MAX_CHUNK_SIZE >= fileSize\n}\n\nfunction validateOffset(\n  files: UploadedFile[],\n  fileName: string,\n  offset: number,\n): UploadedFile {\n  const validFile = files.find(\n    (file) => getFileName(file) === fileName && offset <= file.size,\n  )\n  if (!validFile) {\n    throw new Error('invalid file offset')\n  }\n  return validFile\n}\n\nexport function useUploaderConnections(\n  peer: Peer,\n  files: UploadedFile[],\n  password: string,\n): Array<UploaderConnection> {\n  const [connections, setConnections] = useState<Array<UploaderConnection>>([])\n\n  useEffect(() => {\n    console.log(\n      '[UploaderConnections] initializing with',\n      files.length,\n      'files',\n    )\n    const cleanupHandlers: Array<() => void> = []\n\n    const listener = (conn: DataConnection) => {\n      console.log('[UploaderConnections] new connection from peer', conn.peer)\n      // If the connection is a report, we need to hard-redirect the uploader to the reported page to prevent them from uploading more files.\n      if (conn.metadata?.type === 'report') {\n        console.log(\n          '[UploaderConnections] received report connection, redirecting',\n        )\n        // Broadcast report message to all connections\n        connections.forEach((c) => {\n          c.dataConnection.send({\n            type: MessageType.Report,\n          })\n          c.dataConnection.close()\n        })\n\n        // Hard-redirect uploader to reported page\n        window.location.href = '/reported'\n        return\n      }\n\n      let sendChunkTimeout: NodeJS.Timeout | null = null\n      const newConn = {\n        status: UploaderConnectionStatus.Pending,\n        dataConnection: conn,\n        completedFiles: 0,\n        totalFiles: files.length,\n        currentFileProgress: 0,\n        acknowledgedBytes: 0,\n      }\n\n      setConnections((conns) => {\n        return [newConn, ...conns]\n      })\n\n      const updateConnection = (\n        fn: (c: UploaderConnection) => UploaderConnection,\n      ) => {\n        setConnections((conns) =>\n          conns.map((c) => (c.dataConnection === conn ? fn(c) : c)),\n        )\n      }\n\n      const onData = (data: any): void => {\n        try {\n          const message = decodeMessage(data)\n          console.log('[UploaderConnections] received message:', message.type)\n          switch (message.type) {\n            case MessageType.RequestInfo: {\n              console.log('[UploaderConnections] client info:', {\n                browser: `${message.browserName} ${message.browserVersion}`,\n                os: `${message.osName} ${message.osVersion}`,\n                mobile: message.mobileVendor\n                  ? `${message.mobileVendor} ${message.mobileModel}`\n                  : 'N/A',\n              })\n              const newConnectionState = {\n                browserName: message.browserName,\n                browserVersion: message.browserVersion,\n                osName: message.osName,\n                osVersion: message.osVersion,\n                mobileVendor: message.mobileVendor,\n                mobileModel: message.mobileModel,\n              }\n\n              if (password) {\n                console.log(\n                  '[UploaderConnections] password required, requesting authentication',\n                )\n                const request: Message = {\n                  type: MessageType.PasswordRequired,\n                }\n                conn.send(request)\n\n                updateConnection((draft) => {\n                  if (draft.status !== UploaderConnectionStatus.Pending) {\n                    return draft\n                  }\n\n                  return {\n                    ...draft,\n                    ...newConnectionState,\n                    status: UploaderConnectionStatus.Authenticating,\n                  }\n                })\n\n                return\n              }\n\n              updateConnection((draft) => {\n                if (draft.status !== UploaderConnectionStatus.Pending) {\n                  return draft\n                }\n\n                return {\n                  ...draft,\n                  ...newConnectionState,\n                  status: UploaderConnectionStatus.Ready,\n                }\n              })\n\n              const fileInfo = files.map((f) => {\n                return {\n                  fileName: getFileName(f),\n                  size: f.size,\n                  type: f.type,\n                }\n              })\n\n              console.log('[UploaderConnections] sending file info:', fileInfo)\n              const request: Message = {\n                type: MessageType.Info,\n                files: fileInfo,\n              }\n\n              conn.send(request)\n              break\n            }\n\n            case MessageType.UsePassword: {\n              console.log('[UploaderConnections] password attempt received')\n              const { password: submittedPassword } = message\n              if (submittedPassword === password) {\n                console.log('[UploaderConnections] password correct')\n                updateConnection((draft) => {\n                  if (\n                    draft.status !== UploaderConnectionStatus.Authenticating &&\n                    draft.status !== UploaderConnectionStatus.InvalidPassword\n                  ) {\n                    return draft\n                  }\n\n                  return {\n                    ...draft,\n                    status: UploaderConnectionStatus.Ready,\n                  }\n                })\n\n                const fileInfo = files.map((f) => ({\n                  fileName: getFileName(f),\n                  size: f.size,\n                  type: f.type,\n                }))\n\n                const request: Message = {\n                  type: MessageType.Info,\n                  files: fileInfo,\n                }\n\n                conn.send(request)\n              } else {\n                console.log('[UploaderConnections] password incorrect')\n                updateConnection((draft) => {\n                  if (\n                    draft.status !== UploaderConnectionStatus.Authenticating\n                  ) {\n                    return draft\n                  }\n\n                  return {\n                    ...draft,\n                    status: UploaderConnectionStatus.InvalidPassword,\n                  }\n                })\n\n                const request: Message = {\n                  type: MessageType.PasswordRequired,\n                  errorMessage: 'Invalid password',\n                }\n                conn.send(request)\n              }\n              break\n            }\n\n            case MessageType.Start: {\n              const fileName = message.fileName\n              let offset = message.offset\n              console.log(\n                '[UploaderConnections] starting transfer of',\n                fileName,\n                'from offset',\n                offset,\n              )\n              const file = validateOffset(files, fileName, offset)\n\n              let chunkCount = 0\n              const sendNextChunkAsync = () => {\n                sendChunkTimeout = setTimeout(() => {\n                  const end = Math.min(file.size, offset + MAX_CHUNK_SIZE)\n                  const final = isFinalChunk(offset, file.size)\n                  chunkCount++\n                  // Log for e2e testing\n                  console.log(\n                    `[UploaderConnections] sending chunk ${chunkCount} for ${fileName} (${offset}-${end}/${file.size}) final=${final}`,\n                  )\n                  const request: Message = {\n                    type: MessageType.Chunk,\n                    fileName,\n                    offset,\n                    bytes: file.slice(offset, end),\n                    final,\n                  }\n                  conn.send(request)\n\n                  updateConnection((draft) => {\n                    offset = end\n                    if (final) {\n                      console.log(\n                        '[UploaderConnections] completed file',\n                        fileName,\n                        '- file',\n                        draft.completedFiles + 1,\n                        'of',\n                        draft.totalFiles,\n                      )\n                      return {\n                        ...draft,\n                        status: UploaderConnectionStatus.Ready,\n                        completedFiles: draft.completedFiles + 1,\n                        currentFileProgress: 0,\n                      }\n                    } else {\n                      sendNextChunkAsync()\n                      return {\n                        ...draft,\n                        uploadingOffset: end,\n                        currentFileProgress: end / file.size,\n                      }\n                    }\n                  })\n                }, 0)\n              }\n\n              updateConnection((draft) => {\n                if (\n                  draft.status !== UploaderConnectionStatus.Ready &&\n                  draft.status !== UploaderConnectionStatus.Paused\n                ) {\n                  return draft\n                }\n\n                sendNextChunkAsync()\n\n                return {\n                  ...draft,\n                  status: UploaderConnectionStatus.Uploading,\n                  uploadingFileName: fileName,\n                  uploadingOffset: offset,\n                  acknowledgedBytes: 0, // Reset acknowledged bytes for new file\n                  currentFileProgress: 0, // Progress based on acks, not sends\n                }\n              })\n\n              break\n            }\n\n            case MessageType.Pause: {\n              console.log('[UploaderConnections] transfer paused')\n              updateConnection((draft) => {\n                if (draft.status !== UploaderConnectionStatus.Uploading) {\n                  return draft\n                }\n\n                if (sendChunkTimeout) {\n                  clearTimeout(sendChunkTimeout)\n                  sendChunkTimeout = null\n                }\n\n                return {\n                  ...draft,\n                  status: UploaderConnectionStatus.Paused,\n                }\n              })\n              break\n            }\n\n            case MessageType.ChunkAck: {\n              const ackMessage = message as z.infer<typeof ChunkAckMessage>\n              console.log(\n                '[UploaderConnections] received chunk ack:',\n                ackMessage.fileName,\n                'offset',\n                ackMessage.offset,\n                'bytes',\n                ackMessage.bytesReceived,\n              )\n\n              updateConnection((draft) => {\n                const currentAcked = draft.acknowledgedBytes || 0\n                const newAcked = currentAcked + ackMessage.bytesReceived\n\n                // Find the file to calculate progress\n                const file = files.find(\n                  (f) => getFileName(f) === ackMessage.fileName,\n                )\n                if (file) {\n                  const acknowledgedProgress = newAcked / file.size\n                  return {\n                    ...draft,\n                    acknowledgedBytes: newAcked,\n                    currentFileProgress: acknowledgedProgress,\n                  }\n                }\n\n                return {\n                  ...draft,\n                  acknowledgedBytes: newAcked,\n                }\n              })\n              break\n            }\n\n            case MessageType.Done: {\n              console.log(\n                '[UploaderConnections] transfer completed successfully',\n              )\n              updateConnection((draft) => {\n                if (draft.status !== UploaderConnectionStatus.Ready) {\n                  return draft\n                }\n\n                conn.close()\n                return {\n                  ...draft,\n                  status: UploaderConnectionStatus.Done,\n                }\n              })\n              break\n            }\n          }\n        } catch (err) {\n          console.error('[UploaderConnections] error handling message:', err)\n        }\n      }\n\n      const onClose = (): void => {\n        console.log('[UploaderConnections] connection closed')\n        if (sendChunkTimeout) {\n          clearTimeout(sendChunkTimeout)\n        }\n\n        updateConnection((draft) => {\n          if (\n            [\n              UploaderConnectionStatus.InvalidPassword,\n              UploaderConnectionStatus.Done,\n            ].includes(draft.status)\n          ) {\n            return draft\n          }\n\n          return {\n            ...draft,\n            status: UploaderConnectionStatus.Closed,\n          }\n        })\n      }\n\n      conn.on('data', onData)\n      conn.on('close', onClose)\n\n      cleanupHandlers.push(() => {\n        conn.off('data', onData)\n        conn.off('close', onClose)\n        conn.close()\n      })\n    }\n\n    peer.on('connection', listener)\n\n    return () => {\n      console.log('[UploaderConnections] cleaning up connections')\n      peer.off('connection', listener)\n      cleanupHandlers.forEach((fn) => fn())\n    }\n  }, [peer, files, password])\n\n  return connections\n}\n"
  },
  {
    "path": "src/log.ts",
    "content": "import debug from 'debug'\n\nexport const error = debug('filepizza:error')\n\nexport const info = debug('filepizza:info')\n\nexport const warn = debug('filepizza:warn')\n"
  },
  {
    "path": "src/messages.ts",
    "content": "import { z } from 'zod'\n\nexport enum MessageType {\n  RequestInfo = 'RequestInfo',\n  Info = 'Info',\n  Start = 'Start',\n  Chunk = 'Chunk',\n  ChunkAck = 'ChunkAck',\n  Pause = 'Pause',\n  Done = 'Done',\n  Error = 'Error',\n  PasswordRequired = 'PasswordRequired',\n  UsePassword = 'UsePassword',\n  Report = 'Report',\n}\n\nexport const RequestInfoMessage = z.object({\n  type: z.literal(MessageType.RequestInfo),\n  browserName: z.string(),\n  browserVersion: z.string(),\n  osName: z.string(),\n  osVersion: z.string(),\n  mobileVendor: z.string(),\n  mobileModel: z.string(),\n})\n\nexport const InfoMessage = z.object({\n  type: z.literal(MessageType.Info),\n  files: z.array(\n    z.object({\n      fileName: z.string(),\n      size: z.number(),\n      type: z.string(),\n    }),\n  ),\n})\n\nexport const StartMessage = z.object({\n  type: z.literal(MessageType.Start),\n  fileName: z.string(),\n  offset: z.number(),\n})\n\nexport const ChunkMessage = z.object({\n  type: z.literal(MessageType.Chunk),\n  fileName: z.string(),\n  offset: z.number(),\n  bytes: z.unknown(),\n  final: z.boolean(),\n})\n\nexport const ChunkAckMessage = z.object({\n  type: z.literal(MessageType.ChunkAck),\n  fileName: z.string(),\n  offset: z.number(),\n  bytesReceived: z.number(),\n})\n\nexport const DoneMessage = z.object({\n  type: z.literal(MessageType.Done),\n})\n\nexport const ErrorMessage = z.object({\n  type: z.literal(MessageType.Error),\n  error: z.string(),\n})\n\nexport const PasswordRequiredMessage = z.object({\n  type: z.literal(MessageType.PasswordRequired),\n  errorMessage: z.string().optional(),\n})\n\nexport const UsePasswordMessage = z.object({\n  type: z.literal(MessageType.UsePassword),\n  password: z.string(),\n})\n\nexport const PauseMessage = z.object({\n  type: z.literal(MessageType.Pause),\n})\n\nexport const ReportMessage = z.object({\n  type: z.literal(MessageType.Report),\n})\n\nexport const Message = z.discriminatedUnion('type', [\n  RequestInfoMessage,\n  InfoMessage,\n  StartMessage,\n  ChunkMessage,\n  ChunkAckMessage,\n  DoneMessage,\n  ErrorMessage,\n  PasswordRequiredMessage,\n  UsePasswordMessage,\n  PauseMessage,\n  ReportMessage,\n])\n\nexport type Message = z.infer<typeof Message>\n\nexport function decodeMessage(data: unknown): Message {\n  return Message.parse(data)\n}\n"
  },
  {
    "path": "src/redisClient.ts",
    "content": "import Redis from 'ioredis'\n\nexport { Redis }\n\nlet redisClient: Redis | null = null\n\nexport function getRedisClient(): Redis {\n  if (!redisClient) {\n    redisClient = process.env.REDIS_URL\n      ? new Redis(process.env.REDIS_URL)\n      : new Redis()\n  }\n  return redisClient\n}\n"
  },
  {
    "path": "src/routes.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next'\nimport config from './config'\n\nexport type APIError = Error & { statusCode?: number }\n\nexport type BodyKey = keyof typeof config.bodyKeys\n\nexport function throwAPIError(message: string, statusCode = 500): void {\n  const err = new Error(message) as APIError\n  err.statusCode = statusCode\n  throw err\n}\n\nexport function routeHandler<T>(\n  fn: (req: NextApiRequest, res: NextApiResponse) => Promise<T>,\n): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {\n  return async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {\n    if (req.method !== 'POST') {\n      res.statusCode = 405\n      res.json({ error: 'method not allowed' })\n      return\n    }\n\n    try {\n      const result = await fn(req, res)\n      res.statusCode = 200\n      res.json(result)\n    } catch (err) {\n      res.statusCode = err.statusCode || 500\n      res.json({ error: err.message })\n    }\n  }\n}\n\nexport function getBodyKey(req: NextApiRequest, key: BodyKey): string {\n  const { min, max } = config.bodyKeys[key]\n\n  const val = req.body[key]\n\n  if (typeof val !== 'string') {\n    throwAPIError(`${key} must be a string`)\n  }\n\n  if (val.length < min) {\n    throwAPIError(`${key} must be at least ${min} chars`)\n  }\n\n  if (val.length > max) {\n    throwAPIError(`${key} must be at most ${max} chars`)\n  }\n\n  return val\n}\n"
  },
  {
    "path": "src/slugs.ts",
    "content": "import 'server-only'\nimport crypto from 'crypto'\nimport config from './config'\n\n/**\n * Generates an array of random words from a given word list.\n *\n * @param wordList - An array of words to choose from.\n * @param numWords - The number of words to generate.\n * @returns A Promise that resolves to an array of randomly selected words.\n */\nfunction generateRandomWords(\n  wordList: string[],\n  numWords: number,\n): Promise<string[]> {\n  return new Promise((resolve, reject) => {\n    if (!Array.isArray(wordList) || wordList.length === 0) {\n      reject(new Error('Word list must be a non-empty array'))\n      return\n    }\n\n    if (numWords <= 0) {\n      reject(new Error('Number of words must be greater than zero'))\n      return\n    }\n\n    const getRandomInt = (max: number): number => {\n      const limit = 4294967295 - (4294967295 % max) // uint32 max\n\n      let buffer = new Uint32Array(1)\n      do {\n        if (typeof window !== 'undefined' && window.crypto) {\n          window.crypto.getRandomValues(buffer)\n        } else {\n          crypto.randomFillSync(buffer)\n        }\n      } while (buffer[0] >= limit)\n\n      return buffer[0] % max\n    }\n\n    const result: string[] = []\n    for (let i = 0; i < numWords; i++) {\n      const randomIndex = getRandomInt(wordList.length)\n      result.push(wordList[randomIndex])\n    }\n\n    resolve(result)\n  })\n}\n\nexport const generateShortSlug = async (): Promise<string> => {\n  const parts = await generateRandomWords(\n    config.shortSlug.chars.split(''),\n    config.shortSlug.numChars,\n  )\n  return parts.join('')\n}\n\nexport const generateLongSlug = async (): Promise<string> => {\n  const parts = await generateRandomWords(\n    config.longSlug.words,\n    config.longSlug.numWords,\n  )\n  return parts.join('/')\n}\n"
  },
  {
    "path": "src/styles.css",
    "content": "@import 'tailwindcss';\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n@theme {\n  --animate-spin-slow: spin 16s linear infinite;\n}\n\nhtml {\n  height: 100dvh;\n}\n\nbody {\n  min-height: 100dvh;\n}\n\n#__next {\n  display: flex;\n  flex-direction: column;\n  height: 100dvh;\n}\n"
  },
  {
    "path": "src/toppings.ts",
    "content": "export default [\n  'alfalfa',\n  'almonds',\n  'anchovies',\n  'artichoke',\n  'avocado',\n  'bacon',\n  'basil',\n  'bayleaves',\n  'bbqchicken',\n  'beans',\n  'beef',\n  'beetroot',\n  'bluecheese',\n  'brie',\n  'broccoli',\n  'cajunchicken',\n  'camembert',\n  'capers',\n  'capicolla',\n  'cardamon',\n  'carrot',\n  'cauliflower',\n  'cheddar',\n  'chickenmasala',\n  'chickentikka',\n  'chili',\n  'chives',\n  'chorizo',\n  'cilantro',\n  'colby',\n  'coriander',\n  'crayfish',\n  'cumin',\n  'dill',\n  'duck',\n  'eggplant',\n  'fenugreek',\n  'feta',\n  'fungi',\n  'garlic',\n  'goatcheese',\n  'gorgonzola',\n  'gouda',\n  'ham',\n  'jalapeno',\n  'laurel',\n  'leeks',\n  'lettuce',\n  'limburger',\n  'lobster',\n  'manchego',\n  'marjoram',\n  'meatballs',\n  'melon',\n  'montereyjack',\n  'mozzarella',\n  'muenster',\n  'mushrooms',\n  'olives',\n  'onion',\n  'oregano',\n  'oysters',\n  'parsley',\n  'parmesan',\n  'peanuts',\n  'peas',\n  'pecans',\n  'pepperoni',\n  'peppers',\n  'pineapple',\n  'pinenuts',\n  'pistachios',\n  'prawn',\n  'prosciutto',\n  'provolone',\n  'ricotta',\n  'romano',\n  'roquefort',\n  'rosemary',\n  'salami',\n  'salmon',\n  'sausage',\n  'scallions',\n  'shallots',\n  'shrimp',\n  'snowpeas',\n  'spinach',\n  'squash',\n  'squid',\n  'sweetcorn',\n  'tomatoes',\n  'tuna',\n  'turkey',\n  'venison',\n  'walnuts',\n  'watercress',\n  'whitebait',\n  'zucchini',\n]\n"
  },
  {
    "path": "src/types.ts",
    "content": "import type { DataConnection } from 'peerjs'\n\nexport type UploadedFile = File & { entryFullPath?: string }\n\nexport enum UploaderConnectionStatus {\n  Pending = 'PENDING',\n  Ready = 'READY',\n  Paused = 'PAUSED',\n  Uploading = 'UPLOADING',\n  Done = 'DONE',\n  Authenticating = 'AUTHENTICATING',\n  InvalidPassword = 'INVALID_PASSWORD',\n  Closed = 'CLOSED',\n}\n\nexport type UploaderConnection = {\n  status: UploaderConnectionStatus\n  dataConnection: DataConnection\n  browserName?: string\n  browserVersion?: string\n  osName?: string\n  osVersion?: string\n  mobileVendor?: string\n  mobileModel?: string\n  uploadingFileName?: string\n  uploadingOffset?: number\n  acknowledgedBytes?: number\n  completedFiles: number\n  totalFiles: number\n  currentFileProgress: number\n}\n"
  },
  {
    "path": "src/utils/download.ts",
    "content": "import { createZipStream } from '../zip-stream'\n\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nif (typeof window !== 'undefined') require('web-streams-polyfill/polyfill')\n\nconst streamSaver =\n  // eslint-disable-next-line @typescript-eslint/no-require-imports\n  typeof window !== 'undefined' ? require('streamsaver') : null\nif (typeof window !== 'undefined') {\n  streamSaver.mitm = `${window.location.protocol}//${window.location.host}/stream.html`\n}\n\ntype DownloadFileStream = {\n  name: string\n  size: number\n  stream: () => ReadableStream<Uint8Array>\n}\n\nexport async function streamDownloadSingleFile(\n  file: DownloadFileStream,\n  filename: string,\n): Promise<void> {\n  const fileStream = streamSaver.createWriteStream(filename, {\n    size: file.size,\n  })\n\n  const writer = fileStream.getWriter()\n  const reader = file.stream().getReader()\n\n  const pump = async () => {\n    const res = await reader.read()\n    return res.done ? writer.close() : writer.write(res.value).then(pump)\n  }\n  await pump()\n}\n\nexport function streamDownloadMultipleFiles(\n  files: Array<DownloadFileStream>,\n  filename: string,\n): Promise<void> {\n  const totalSize = files.reduce((acc, file) => acc + file.size, 0)\n  const fileStream = streamSaver.createWriteStream(filename, {\n    size: totalSize,\n  })\n\n  const readableZipStream = createZipStream({\n    start(ctrl) {\n      for (const file of files) {\n        ctrl.enqueue(file as unknown as ArrayBufferView)\n      }\n      ctrl.close()\n    },\n    async pull(_ctrl) {\n      // Gets executed everytime zip-stream asks for more data\n    },\n  })\n\n  return readableZipStream.pipeTo(fileStream)\n}\n"
  },
  {
    "path": "src/utils/pluralize.ts",
    "content": "export function pluralize(\n  count: number,\n  singular: string,\n  plural: string,\n): string {\n  return `${count} ${count === 1 ? singular : plural}`\n}\n"
  },
  {
    "path": "src/zip-stream.ts",
    "content": "// Based on https://github.com/jimmywarting/StreamSaver.js/blob/master/examples/zip-stream.js\n//\n// Disabling typechecking for now since this was originally JavaScript, should\n// find some time to add types later.\n//\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\n\nclass Crc32 {\n  crc: number\n\n  constructor() {\n    this.crc = -1\n  }\n\n  table = (() => {\n    let i\n    let j\n    let t\n    const table = []\n    for (i = 0; i < 256; i++) {\n      t = i\n      for (j = 0; j < 8; j++) {\n        t = t & 1 ? (t >>> 1) ^ 0xedb88320 : t >>> 1\n      }\n      table[i] = t\n    }\n    return table\n  })()\n\n  append(data) {\n    let crc = this.crc | 0\n    const table = this.table\n    for (let offset = 0, len = data.length | 0; offset < len; offset++) {\n      crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xff]\n    }\n    this.crc = crc\n  }\n\n  get() {\n    return ~this.crc\n  }\n}\n\nconst getDataHelper = (byteLength) => {\n  const uint8 = new Uint8Array(byteLength)\n  return {\n    array: uint8,\n    view: new DataView(uint8.buffer),\n  }\n}\n\nconst pump = (zipObj) =>\n  zipObj.reader.read().then((chunk) => {\n    if (chunk.done) return zipObj.writeFooter()\n    const outputData = chunk.value\n    zipObj.crc.append(outputData)\n    zipObj.uncompressedLength += outputData.length\n    zipObj.compressedLength += outputData.length\n    zipObj.ctrl.enqueue(outputData)\n  })\n\nexport function createZipStream(\n  underlyingSource: UnderlyingSource<any>,\n): ReadableStream {\n  const files = Object.create(null)\n  const filenames = []\n  const encoder = new TextEncoder()\n  let offset = 0\n  let activeZipIndex = 0\n  let ctrl\n  let activeZipObject, closed\n\n  function next() {\n    activeZipIndex++\n    activeZipObject = files[filenames[activeZipIndex]]\n    // eslint-disable-next-line @typescript-eslint/no-use-before-define\n    if (activeZipObject) processNextChunk()\n    // eslint-disable-next-line @typescript-eslint/no-use-before-define\n    else if (closed) closeZip()\n  }\n\n  const zipWriter = {\n    enqueue(fileLike) {\n      if (closed)\n        throw new TypeError(\n          'Cannot enqueue a chunk into a readable stream that is closed or has been requested to be closed',\n        )\n\n      let name = fileLike.name.trim()\n      const date = new Date(\n        typeof fileLike.lastModified === 'undefined'\n          ? Date.now()\n          : fileLike.lastModified,\n      )\n\n      if (fileLike.directory && !name.endsWith('/')) name += '/'\n      if (files[name]) throw new Error('File already exists.')\n\n      const nameBuf = encoder.encode(name)\n      filenames.push(name)\n\n      const zipObject = (files[name] = {\n        level: 0,\n        ctrl,\n        directory: !!fileLike.directory,\n        nameBuf,\n        comment: encoder.encode(fileLike.comment || ''),\n        compressedLength: 0,\n        uncompressedLength: 0,\n        writeHeader() {\n          const header = getDataHelper(26)\n          const data = getDataHelper(30 + nameBuf.length)\n\n          zipObject.offset = offset\n          zipObject.header = header\n          if (zipObject.level !== 0 && !zipObject.directory) {\n            header.view.setUint16(4, 0x0800)\n          }\n          header.view.setUint32(0, 0x14000808)\n          header.view.setUint16(\n            6,\n            (((date.getHours() << 6) | date.getMinutes()) << 5) |\n              (date.getSeconds() / 2),\n            true,\n          )\n          header.view.setUint16(\n            8,\n            ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) <<\n              5) |\n              date.getDate(),\n            true,\n          )\n          header.view.setUint16(22, nameBuf.length, true)\n          data.view.setUint32(0, 0x504b0304)\n          data.array.set(header.array, 4)\n          data.array.set(nameBuf, 30)\n          offset += data.array.length\n          ctrl.enqueue(data.array)\n        },\n        writeFooter() {\n          const footer = getDataHelper(16)\n          footer.view.setUint32(0, 0x504b0708)\n\n          if (zipObject.crc) {\n            zipObject.header.view.setUint32(10, zipObject.crc.get(), true)\n            zipObject.header.view.setUint32(\n              14,\n              zipObject.compressedLength,\n              true,\n            )\n            zipObject.header.view.setUint32(\n              18,\n              zipObject.uncompressedLength,\n              true,\n            )\n            footer.view.setUint32(4, zipObject.crc.get(), true)\n            footer.view.setUint32(8, zipObject.compressedLength, true)\n            footer.view.setUint32(12, zipObject.uncompressedLength, true)\n          }\n\n          ctrl.enqueue(footer.array)\n          offset += zipObject.compressedLength + 16\n          next()\n        },\n        fileLike,\n      })\n\n      if (!activeZipObject) {\n        activeZipObject = zipObject\n        // eslint-disable-next-line @typescript-eslint/no-use-before-define\n        processNextChunk()\n      }\n    },\n    close() {\n      if (closed)\n        throw new TypeError(\n          'Cannot close a readable stream that has already been requested to be closed',\n        )\n      // eslint-disable-next-line @typescript-eslint/no-use-before-define\n      if (!activeZipObject) closeZip()\n      closed = true\n    },\n  }\n\n  function closeZip() {\n    let length = 0\n    let index = 0\n    let indexFilename, file\n    for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {\n      file = files[filenames[indexFilename]]\n      length += 46 + file.nameBuf.length + file.comment.length\n    }\n    const data = getDataHelper(length + 22)\n    for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {\n      file = files[filenames[indexFilename]]\n      data.view.setUint32(index, 0x504b0102)\n      data.view.setUint16(index + 4, 0x1400)\n      data.array.set(file.header.array, index + 6)\n      data.view.setUint16(index + 32, file.comment.length, true)\n      if (file.directory) {\n        data.view.setUint8(index + 38, 0x10)\n      }\n      data.view.setUint32(index + 42, file.offset, true)\n      data.array.set(file.nameBuf, index + 46)\n      data.array.set(file.comment, index + 46 + file.nameBuf.length)\n      index += 46 + file.nameBuf.length + file.comment.length\n    }\n    data.view.setUint32(index, 0x504b0506)\n    data.view.setUint16(index + 8, filenames.length, true)\n    data.view.setUint16(index + 10, filenames.length, true)\n    data.view.setUint32(index + 12, length, true)\n    data.view.setUint32(index + 16, offset, true)\n    ctrl.enqueue(data.array)\n    ctrl.close()\n  }\n\n  function processNextChunk() {\n    if (!activeZipObject) return\n    if (activeZipObject.directory)\n      return activeZipObject.writeFooter(activeZipObject.writeHeader())\n    if (activeZipObject.reader) return pump(activeZipObject)\n    if (activeZipObject.fileLike.stream) {\n      activeZipObject.crc = new Crc32()\n      activeZipObject.reader = activeZipObject.fileLike.stream().getReader()\n      activeZipObject.writeHeader()\n    } else next()\n  }\n  return new ReadableStream({\n    start: (c) => {\n      ctrl = c\n      if (underlyingSource.start)\n        Promise.resolve(underlyingSource.start(zipWriter))\n    },\n    pull() {\n      return (\n        processNextChunk() ||\n        (underlyingSource.pull &&\n          Promise.resolve(underlyingSource.pull(zipWriter)))\n      )\n    },\n  })\n}\n"
  },
  {
    "path": "tests/e2e/add-files.test.ts",
    "content": "/// <reference types=\"@playwright/test\" />\nimport { test, expect } from '@playwright/test'\nimport { createTestFile, uploadFile, addFile } from './helpers'\n\ntest('user can add more files before starting upload', async ({ page }) => {\n  const file1 = createTestFile('first.txt', 'A')\n  const file2 = createTestFile('second.txt', 'B')\n\n  await uploadFile(page, file1)\n\n  // Add another file using the add files button\n  await addFile(page, file2)\n\n  // Both files should be listed\n  await expect(page.getByText(file1.name)).toBeVisible()\n  await expect(page.getByText(file2.name)).toBeVisible()\n})\n"
  },
  {
    "path": "tests/e2e/basic.test.ts",
    "content": "/// <reference types=\"@playwright/test\" />\nimport { test, expect } from '@playwright/test'\n\ntest('home page loads', async ({ page }) => {\n  await page.goto('http://localhost:3000/')\n  await expect(\n    page.getByText('Peer-to-peer file transfers in your browser.'),\n  ).toBeVisible()\n})\n"
  },
  {
    "path": "tests/e2e/file-transfer.test.ts",
    "content": "/// <reference types=\"@playwright/test\" />\nimport { test, expect } from '@playwright/test'\nimport {\n  createTestFile,\n  uploadFile,\n  startUpload,\n  downloadFile,\n  verifyFileIntegrity,\n  verifyTransferCompletion,\n  createBrowserContexts,\n  monitorChunkProgress,\n  verifyPreciseProgress,\n} from './helpers'\n\ninterface TestCase {\n  name: string\n  fileSizeMultiplier: number\n  expectedChunks: number\n  fillChar: string\n}\n\nconst CHUNK_SIZE = 256 * 1024 // 256 KB\n\nconst testCases: TestCase[] = [\n  {\n    name: 'tiny file (basic transfer)',\n    fileSizeMultiplier: 0.1, // ~26KB\n    expectedChunks: 1,\n    fillChar: 'T',\n  },\n  {\n    name: 'small file (single chunk)',\n    fileSizeMultiplier: 0.5, // 128KB\n    expectedChunks: 1,\n    fillChar: 'S',\n  },\n  {\n    name: 'medium file (3 chunks)',\n    fileSizeMultiplier: 2.5, // 640KB\n    expectedChunks: 3,\n    fillChar: 'M',\n  },\n  {\n    name: 'large file (4 chunks)',\n    fileSizeMultiplier: 4, // 1024KB\n    expectedChunks: 4,\n    fillChar: 'L',\n  },\n  {\n    name: 'extra large file (7 chunks)',\n    fileSizeMultiplier: 6.5, // ~1664KB\n    expectedChunks: 7,\n    fillChar: 'X',\n  },\n]\n\nfor (const testCase of testCases) {\n  test(`file transfer: ${testCase.name}`, async ({ browser }) => {\n    const fileSize = Math.floor(CHUNK_SIZE * testCase.fileSizeMultiplier)\n    const testFile = createTestFile(\n      `test-${testCase.fillChar.toLowerCase()}-${testCase.expectedChunks}chunks.txt`,\n      testCase.fillChar.repeat(fileSize)\n    )\n\n    const { uploaderPage, downloaderPage, cleanup } = await createBrowserContexts(browser)\n\n    try {\n      // Set up precise chunk and progress monitoring\n      const monitor = await monitorChunkProgress(uploaderPage, downloaderPage, fileSize)\n\n      await uploadFile(uploaderPage, testFile)\n      const shareUrl = await startUpload(uploaderPage)\n      const downloadPath = await downloadFile(downloaderPage, shareUrl, testFile)\n      \n      await verifyFileIntegrity(downloadPath, testFile)\n      await verifyTransferCompletion(downloaderPage)\n\n      // Wait for all async progress captures to complete\n      await downloaderPage.waitForTimeout(1000)\n\n      // Verify precise progress tracking for both upload and download\n      verifyPreciseProgress(monitor.uploadChunks, testCase.expectedChunks, 'upload')\n      verifyPreciseProgress(monitor.downloadChunks, testCase.expectedChunks, 'download')\n\n      // Verify final completion shows exactly 100% on both sides\n      await expect(uploaderPage.locator('#progress-percentage')).toHaveText('100%')\n      await expect(downloaderPage.locator('#progress-percentage')).toHaveText('100%')\n\n    } finally {\n      await cleanup()\n    }\n  })\n}\n"
  },
  {
    "path": "tests/e2e/helpers.ts",
    "content": "import { Page, Browser, expect } from '@playwright/test'\nimport { createHash } from 'crypto'\nimport { writeFileSync, readFileSync } from 'fs'\nimport { join } from 'path'\nimport { tmpdir } from 'os'\n\nexport interface TestFile {\n  name: string\n  content: string\n  path: string\n  checksum: string\n}\n\nexport function createTestFile(fileName: string, content: string): TestFile {\n  const testFilePath = join(tmpdir(), fileName)\n  writeFileSync(testFilePath, content)\n\n  const checksum = createHash('sha256').update(content).digest('hex')\n\n  return {\n    name: fileName,\n    content,\n    path: testFilePath,\n    checksum,\n  }\n}\n\nexport async function uploadFile(\n  page: Page,\n  testFile: TestFile,\n): Promise<void> {\n  // Navigate to home page\n  await page.goto('http://localhost:3000/')\n  await expect(\n    page.getByText('Peer-to-peer file transfers in your browser.'),\n  ).toBeVisible()\n\n  // Wait for drop zone button to be ready\n  await expect(page.locator('#drop-zone-button')).toBeVisible()\n\n  // Upload file using the file input and trigger change event\n  await page.evaluate(\n    ({ testContent, testFileName }) => {\n      const input = document.querySelector(\n        'input[type=\"file\"]',\n      ) as HTMLInputElement\n      if (input) {\n        const file = new File([testContent], testFileName, {\n          type: 'text/plain',\n        })\n        const dataTransfer = new DataTransfer()\n        dataTransfer.items.add(file)\n        input.files = dataTransfer.files\n\n        // Manually trigger the change event\n        const event = new Event('change', { bubbles: true })\n        input.dispatchEvent(event)\n      }\n    },\n    { testContent: testFile.content, testFileName: testFile.name },\n  )\n\n  // Wait for file to be processed and confirm upload page to appear\n  await expect(page.getByText(/You are about to start uploading/i)).toBeVisible(\n    { timeout: 10000 },\n  )\n}\n\nexport async function addFile(\n  page: Page,\n  testFile: TestFile,\n): Promise<void> {\n  await page.evaluate(\n    ({ testContent, testFileName }) => {\n      const input = document.querySelector(\n        '#add-files-input',\n      ) as HTMLInputElement\n      if (input) {\n        const file = new File([testContent], testFileName, {\n          type: 'text/plain',\n        })\n        const dataTransfer = new DataTransfer()\n        dataTransfer.items.add(file)\n        input.files = dataTransfer.files\n\n        const event = new Event('change', { bubbles: true })\n        input.dispatchEvent(event)\n      }\n    },\n    { testContent: testFile.content, testFileName: testFile.name },\n  )\n\n  await expect(page.getByText(testFile.name)).toBeVisible({ timeout: 5000 })\n}\n\nexport async function startUpload(page: Page): Promise<string> {\n  // Start sharing\n  await page.locator('#start-button').click()\n\n  // Wait for uploading state and get the share URL\n  await expect(page.getByText(/You are uploading/i)).toBeVisible({\n    timeout: 10000,\n  })\n\n  // Get the share URL from the copyable input (Long URL)\n  const shareUrlInput = page.locator('#copyable-input-long-url')\n  await expect(shareUrlInput).toBeVisible({ timeout: 5000 })\n  const shareUrl = await shareUrlInput.inputValue()\n\n  expect(shareUrl).toMatch(/http:\\/\\/localhost:3000\\//)\n  return shareUrl\n}\n\nexport async function downloadFile(\n  page: Page,\n  shareUrl: string,\n  testFile: TestFile,\n): Promise<string> {\n  // Navigate to share URL\n  await page.goto(shareUrl)\n\n  // Wait for download page to load\n  await expect(page.getByText(testFile.name)).toBeVisible({\n    timeout: 10000,\n  })\n\n  // Start download\n  const downloadPromise = page.waitForEvent('download')\n  await page.locator('#download-button').click()\n  const download = await downloadPromise\n\n  // Verify download\n  expect(download.suggestedFilename()).toBe(testFile.name)\n\n  // Save downloaded file\n  const downloadPath = join(tmpdir(), `downloaded-${testFile.name}`)\n  await download.saveAs(downloadPath)\n\n  return downloadPath\n}\n\nexport async function verifyFileIntegrity(\n  downloadPath: string,\n  testFile: TestFile,\n): Promise<void> {\n  // Verify downloaded content and checksum\n  const downloadedContent = readFileSync(downloadPath, 'utf8')\n  expect(downloadedContent).toBe(testFile.content)\n\n  const downloadedChecksum = createHash('sha256')\n    .update(downloadedContent)\n    .digest('hex')\n  expect(downloadedChecksum).toBe(testFile.checksum)\n}\n\nexport async function verifyTransferCompletion(\n  downloaderPage: Page,\n): Promise<void> {\n  // Verify download completion on downloader side\n  await expect(downloaderPage.getByText(/You downloaded/i)).toBeVisible({\n    timeout: 10000,\n  })\n}\n\nexport async function createBrowserContexts(browser: Browser): Promise<{\n  uploaderPage: Page\n  downloaderPage: Page\n  cleanup: () => Promise<void>\n}> {\n  const uploaderContext = await browser.newContext()\n  const downloaderContext = await browser.newContext()\n\n  const uploaderPage = await uploaderContext.newPage()\n  const downloaderPage = await downloaderContext.newPage()\n\n  const cleanup = async () => {\n    await uploaderContext.close()\n    await downloaderContext.close()\n  }\n\n  return { uploaderPage, downloaderPage, cleanup }\n}\n\nexport interface ProgressMonitor {\n  uploaderProgress: number\n  downloaderProgress: number\n  maxProgress: number\n}\n\nexport interface ChunkProgressLog {\n  chunkNumber: number\n  fileName: string\n  offset: number\n  end: number\n  fileSize: number\n  final: boolean\n  progressPercentage: number\n  side: 'upload' | 'download'\n}\n\nexport interface PreciseChunkMonitor {\n  uploadChunks: ChunkProgressLog[]\n  downloadChunks: ChunkProgressLog[]\n}\n\nexport async function monitorChunkProgress(\n  uploaderPage: Page,\n  downloaderPage: Page,\n  expectedFileSize: number,\n): Promise<PreciseChunkMonitor> {\n  const uploadChunks: ChunkProgressLog[] = []\n  const downloadChunks: ChunkProgressLog[] = []\n\n  uploaderPage.on('console', async (msg) => {\n    const text = msg.text()\n    if (text.includes('[UploaderConnections] received chunk ack')) {\n      // Parse ack log: \"[UploaderConnections] received chunk ack: file.txt offset 0 bytes 262144\"\n      const ackMatch = text.match(\n        /received chunk ack: (\\S+) offset (\\d+) bytes (\\d+)/,\n      )\n      if (ackMatch) {\n        const [, fileName, offset, bytes] = ackMatch\n\n        // Calculate which chunk this corresponds to and expected progress\n        const chunkNumber = Math.floor(parseInt(offset) / (256 * 1024)) + 1\n        const chunkEnd = parseInt(offset) + parseInt(bytes)\n        const final = chunkEnd >= expectedFileSize\n        const progressPercentage = Math.round(\n          (chunkEnd / expectedFileSize) * 100,\n        )\n\n        uploadChunks.push({\n          chunkNumber,\n          fileName,\n          offset: parseInt(offset),\n          end: chunkEnd,\n          fileSize: expectedFileSize,\n          final,\n          progressPercentage,\n          side: 'upload',\n        })\n      }\n    }\n  })\n\n  downloaderPage.on('console', async (msg) => {\n    const text = msg.text()\n    if (\n      text.includes('[Downloader] received chunk') &&\n      !text.includes('finished receiving')\n    ) {\n      // Parse log: \"[Downloader] received chunk 1 for file.txt (0-262144) final=false\"\n      const chunkMatch = text.match(\n        /received chunk (\\d+) for (\\S+) \\((\\d+)-(\\d+)\\) final=(\\w+)/,\n      )\n      if (chunkMatch) {\n        const [, chunkNum, fileName, offset, end, final] = chunkMatch\n\n        // Calculate expected progress based on chunk data\n        const chunkEnd = parseInt(end)\n        const progressPercentage = Math.round(\n          (chunkEnd / expectedFileSize) * 100,\n        )\n\n        downloadChunks.push({\n          chunkNumber: parseInt(chunkNum),\n          fileName,\n          offset: parseInt(offset),\n          end: chunkEnd,\n          fileSize: expectedFileSize,\n          final: final === 'true',\n          progressPercentage,\n          side: 'download',\n        })\n      }\n    }\n  })\n\n  return {\n    uploadChunks,\n    downloadChunks,\n  }\n}\n\nexport function verifyPreciseProgress(\n  chunks: ChunkProgressLog[],\n  expectedChunks: number,\n  side: 'upload' | 'download',\n): void {\n  expect(chunks.length).toBe(expectedChunks)\n\n  for (let i = 0; i < chunks.length; i++) {\n    const chunk = chunks[i]\n\n    console.log(\n      `${side} chunk ${chunk.chunkNumber}: ${chunk.offset}-${chunk.end}/${chunk.fileSize} ` +\n        `progress=${chunk.progressPercentage}% final=${chunk.final}`,\n    )\n\n    // Verify chunks are received in order\n    expect(chunk.chunkNumber).toBe(i + 1)\n\n    // Verify progress is monotonically increasing\n    if (i > 0) {\n      expect(chunk.progressPercentage).toBeGreaterThanOrEqual(\n        chunks[i - 1].progressPercentage,\n      )\n    }\n\n    // For the final chunk, ensure we reach exactly 100%\n    if (chunk.final) {\n      expect(chunk.progressPercentage).toBe(100)\n    }\n\n    // Verify progress percentage is reasonable (0-100%)\n    expect(chunk.progressPercentage).toBeGreaterThanOrEqual(0)\n    expect(chunk.progressPercentage).toBeLessThanOrEqual(100)\n  }\n}\n\n"
  },
  {
    "path": "tests/unit/CancelButton.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport CancelButton from '../../src/components/CancelButton'\n\ndescribe('CancelButton', () => {\n  it('calls onClick when clicked', () => {\n    const onClick = vi.fn()\n    const { getByText } = render(<CancelButton onClick={onClick} />)\n    fireEvent.click(getByText('Cancel'))\n    expect(onClick).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "tests/unit/ConnectionListItem.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport { ConnectionListItem } from '../../src/components/ConnectionListItem'\nimport { UploaderConnectionStatus } from '../../src/types'\n\nconst baseConn = {\n  status: UploaderConnectionStatus.Uploading,\n  dataConnection: {} as any,\n  completedFiles: 1,\n  totalFiles: 2,\n  currentFileProgress: 0.5,\n  browserName: 'Chrome',\n  browserVersion: '120',\n}\n\ndescribe('ConnectionListItem', () => {\n  it('shows status and progress', () => {\n    const { getByText } = render(<ConnectionListItem conn={baseConn} />)\n    expect(getByText((c, e) => e?.textContent === 'Chrome v120')).toBeInTheDocument()\n    expect(getByText('UPLOADING')).toBeInTheDocument()\n    expect(getByText('Completed: 1 / 2 files')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/CopyableInput.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { act } from 'react'\nimport { describe, it, expect, vi } from 'vitest'\nimport { CopyableInput } from '../../src/components/CopyableInput'\n\nObject.assign(navigator, {\n  clipboard: {\n    writeText: vi.fn().mockResolvedValue(undefined),\n  },\n})\n\ndescribe('CopyableInput', () => {\n  it('copies text when button clicked', async () => {\n    const { getByText } = render(<CopyableInput label=\"URL\" value=\"hello\" />)\n    await act(async () => {\n      fireEvent.click(getByText('Copy'))\n    })\n    expect(navigator.clipboard.writeText).toHaveBeenCalledWith('hello')\n  })\n})\n"
  },
  {
    "path": "tests/unit/DownloadButton.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport DownloadButton from '../../src/components/DownloadButton'\n\ndescribe('DownloadButton', () => {\n  it('calls onClick', () => {\n    const fn = vi.fn()\n    const { getByText } = render(<DownloadButton onClick={fn} />)\n    fireEvent.click(getByText('Download'))\n    expect(fn).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "tests/unit/Downloader.subcomponents.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport { vi } from 'vitest'\nvi.mock('next-view-transitions', () => ({ Link: (p: any) => <a {...p}>{p.children}</a> }))\nimport React from 'react'\nimport { render, fireEvent, waitFor } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport {\n  ConnectingToUploader,\n  DownloadComplete,\n  DownloadInProgress,\n  ReadyToDownload,\n  PasswordEntry,\n} from '../../src/components/Downloader'\n\nconst files = [{ fileName: 'a.txt', size: 1, type: 'text/plain' }]\n\ndescribe('Downloader subcomponents', () => {\n  it('ConnectingToUploader shows troubleshooting', async () => {\n    const { getByText } = render(\n      <ConnectingToUploader showTroubleshootingAfter={0} />,\n    )\n    await waitFor(() => {\n      expect(getByText('Having trouble connecting?')).toBeInTheDocument()\n    })\n  })\n\n  it('DownloadComplete lists files', () => {\n    const { getByText } = render(\n      <DownloadComplete filesInfo={files} bytesDownloaded={1} totalSize={1} />,\n    )\n    expect(getByText('You downloaded 1 file.')).toBeInTheDocument()\n  })\n\n  it('DownloadInProgress shows stop button', () => {\n    const { getByText } = render(\n      <DownloadInProgress filesInfo={files} bytesDownloaded={0} totalSize={1} onStop={() => {}} />,\n    )\n    expect(getByText('Stop Download')).toBeInTheDocument()\n  })\n\n  it('ReadyToDownload shows start button', () => {\n    const { getByText } = render(\n      <ReadyToDownload filesInfo={files} onStart={() => {}} />,\n    )\n    expect(getByText('Download')).toBeInTheDocument()\n  })\n\n  it('PasswordEntry submits value', () => {\n    let submitted = ''\n    const { getByPlaceholderText, getByText } = render(\n      <PasswordEntry errorMessage={null} onSubmit={(v) => (submitted = v)} />,\n    )\n    fireEvent.change(\n      getByPlaceholderText('Enter a secret password for this slice of FilePizza...'),\n      { target: { value: 'secret' } },\n    )\n    fireEvent.submit(getByText('Unlock'))\n    expect(submitted).toBe('secret')\n  })\n})\n"
  },
  {
    "path": "tests/unit/DropZone.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport DropZone from '../../src/components/DropZone'\n\nfunction createFile(name: string) {\n  return new File(['hello'], name, { type: 'text/plain' })\n}\n\ndescribe('DropZone', () => {\n  it('calls onDrop when file selected', () => {\n    const fn = vi.fn()\n    const { container } = render(<DropZone onDrop={fn} />)\n    const input = container.querySelector('input') as HTMLInputElement\n    fireEvent.change(input, { target: { files: [createFile('a.txt')] } })\n    expect(fn).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "tests/unit/ErrorMessage.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport { ErrorMessage } from '../../src/components/ErrorMessage'\n\ndescribe('ErrorMessage', () => {\n  it('renders message', () => {\n    const { getByText } = render(<ErrorMessage message=\"oops\" />)\n    expect(getByText('oops')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/Footer.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport Footer from '../../src/components/Footer'\n\nObject.defineProperty(window, 'location', {\n  value: { href: '' },\n  writable: true,\n})\n\ndescribe('Footer', () => {\n  it('redirects to donate link', () => {\n    const { getByText } = render(<Footer />)\n    fireEvent.click(getByText('Donate'))\n    expect(window.location.href).toContain('coinbase')\n  })\n})\n"
  },
  {
    "path": "tests/unit/InputLabel.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport InputLabel from '../../src/components/InputLabel'\n\ndescribe('InputLabel', () => {\n  it('shows tooltip on hover', () => {\n    const { getByRole, getByText } = render(\n      <InputLabel tooltip=\"tip\">Label</InputLabel>,\n    )\n    const button = getByRole('button')\n    fireEvent.mouseOver(button)\n    expect(getByText('tip')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/Loading.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport Loading from '../../src/components/Loading'\n\ndescribe('Loading', () => {\n  it('renders text', () => {\n    const { getByText } = render(<Loading text=\"wait\" />)\n    expect(getByText('wait')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/ModeToggle.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport { ModeToggle } from '../../src/components/ModeToggle'\n\nconst setTheme = vi.fn()\nvi.mock('next-themes', () => ({ useTheme: () => ({ setTheme, resolvedTheme: 'light' }) }))\n\ndescribe('ModeToggle', () => {\n  it('toggles theme', () => {\n    const { getByRole } = render(<ModeToggle />)\n    fireEvent.click(getByRole('button'))\n    expect(setTheme).toHaveBeenCalledWith('dark')\n  })\n})\n"
  },
  {
    "path": "tests/unit/PasswordField.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport PasswordField from '../../src/components/PasswordField'\n\ndescribe('PasswordField', () => {\n  it('calls onChange', () => {\n    let val = ''\n    const { getByPlaceholderText } = render(\n      <PasswordField value=\"\" onChange={(v) => (val = v)} />,\n    )\n    fireEvent.change(getByPlaceholderText('Enter a secret password for this slice of FilePizza...'), {\n      target: { value: 'a' },\n    })\n    expect(val).toBe('a')\n  })\n})\n"
  },
  {
    "path": "tests/unit/ProgressBar.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport ProgressBar from '../../src/components/ProgressBar'\n\ndescribe('ProgressBar', () => {\n  it('shows percentage', () => {\n    const { getAllByText } = render(<ProgressBar value={50} max={100} />)\n    expect(getAllByText('50%').length).toBeGreaterThan(0)\n  })\n})\n"
  },
  {
    "path": "tests/unit/QueryClientProvider.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport FilePizzaQueryClientProvider from '../../src/components/QueryClientProvider'\n\ndescribe('QueryClientProvider', () => {\n  it('renders children', () => {\n    const { getByText } = render(\n      <FilePizzaQueryClientProvider>\n        <span>child</span>\n      </FilePizzaQueryClientProvider>,\n    )\n    expect(getByText('child')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/ReportTermsViolationButton.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport FilePizzaQueryClientProvider from '../../src/components/QueryClientProvider'\n\nvi.mock('../../src/components/WebRTCProvider', () => ({\n  useWebRTCPeer: () => ({ peer: { connect: vi.fn(() => ({ on: vi.fn(), close: vi.fn() })) } }),\n}))\n\nimport ReportTermsViolationButton from '../../src/components/ReportTermsViolationButton'\n\ndescribe('ReportTermsViolationButton', () => {\n  it('opens modal on click', () => {\n    const { getByText } = render(\n      <FilePizzaQueryClientProvider>\n        <ReportTermsViolationButton uploaderPeerID=\"peer\" slug=\"slug\" />\n      </FilePizzaQueryClientProvider>,\n    )\n    fireEvent.click(getByText('Report suspicious pizza delivery'))\n    expect(getByText('Found a suspicious delivery?')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/ReturnHome.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nvi.mock(\"next-view-transitions\", () => ({ Link: (p: any) => <a {...p}>{p.children}</a> }))\nimport { vi } from \"vitest\"\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport ReturnHome from '../../src/components/ReturnHome'\n\ndescribe('ReturnHome', () => {\n  it('links to home', () => {\n    const { getByText } = render(<ReturnHome />)\n    expect(getByText(/Serve up/).getAttribute('href')).toBe('/')\n  })\n})\n"
  },
  {
    "path": "tests/unit/Spinner.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { act } from 'react'\nimport { describe, it, expect } from 'vitest'\nimport Spinner from '../../src/components/Spinner'\nimport { setRotating } from '../../src/hooks/useRotatingSpinner'\n\ndescribe('Spinner', () => {\n  it('reflects rotating state', () => {\n    // @ts-ignore\n    act(() => { setRotating(true) })\n// @ts-ignore\n    const { getByLabelText } = render(<Spinner />)\n    expect(getByLabelText('Rotating pizza')).toBeInTheDocument()\n    // @ts-ignore\n    act(() => { setRotating(false) })\n  })\n})\n"
  },
  {
    "path": "tests/unit/StartButton.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport StartButton from '../../src/components/StartButton'\n\ndescribe('StartButton', () => {\n  it('calls handler', () => {\n    const fn = vi.fn()\n    const { getByText } = render(<StartButton onClick={fn} />)\n    fireEvent.click(getByText('Start'))\n    expect(fn).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "tests/unit/StopButton.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport StopButton from '../../src/components/StopButton'\n\ndescribe('StopButton', () => {\n  it('labels correctly when downloading', () => {\n    const { getByText } = render(<StopButton onClick={() => {}} isDownloading />)\n    expect(getByText('Stop Download')).toBeInTheDocument()\n  })\n\n  it('calls handler', () => {\n    const fn = vi.fn()\n    const { getByText } = render(<StopButton onClick={fn} />)\n    fireEvent.click(getByText('Stop Upload'))\n    expect(fn).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "tests/unit/TermsAcceptance.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport TermsAcceptance from '../../src/components/TermsAcceptance'\n\ndescribe('TermsAcceptance', () => {\n  it('opens modal', () => {\n    const { getByText } = render(<TermsAcceptance />)\n    fireEvent.click(getByText('our terms'))\n    expect(getByText('FilePizza Terms')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/ThemeProvider.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nObject.defineProperty(window, \"matchMedia\", { value: () => ({ matches: false, addListener: () => {}, removeListener: () => {} }) })\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport { ThemeProvider } from '../../src/components/ThemeProvider'\n\ndescribe('ThemeProvider', () => {\n  it('renders children', () => {\n    const { getByText } = render(\n      <ThemeProvider>\n        <span>child</span>\n      </ThemeProvider>,\n    )\n    expect(getByText('child')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/TitleText.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport TitleText from '../../src/components/TitleText'\n\ndescribe('TitleText', () => {\n  it('renders children', () => {\n    const { getByText } = render(<TitleText>hello</TitleText>)\n    expect(getByText('hello')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/TypeBadge.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport TypeBadge from '../../src/components/TypeBadge'\n\ndescribe('TypeBadge', () => {\n  it('renders type', () => {\n    const { getByText } = render(<TypeBadge type=\"image/png\" />)\n    expect(getByText('image/png')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/UnlockButton.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport UnlockButton from '../../src/components/UnlockButton'\n\ndescribe('UnlockButton', () => {\n  it('calls onClick', () => {\n    const fn = vi.fn()\n    const { getByText } = render(<UnlockButton onClick={fn} />)\n    fireEvent.click(getByText('Unlock'))\n    expect(fn).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "tests/unit/UploadFileList.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport UploadFileList from '../../src/components/UploadFileList'\n\ndescribe('UploadFileList', () => {\n  it('calls onRemove', () => {\n    const fn = vi.fn()\n    const files = [{ fileName: 'a.txt', type: 'text/plain' }]\n    const { getByText } = render(<UploadFileList files={files} onRemove={fn} />)\n    fireEvent.click(getByText('✕'))\n    expect(fn).toHaveBeenCalledWith(0)\n  })\n})\n"
  },
  {
    "path": "tests/unit/Uploader.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\n\nvar mockUseUploaderChannel: any\n\nvi.mock('../../src/components/WebRTCProvider', () => ({\n  useWebRTCPeer: () => ({ peer: { id: '1' }, stop: vi.fn() }),\n}))\nvi.mock('../../src/hooks/useUploaderChannel', () => ({\n  useUploaderChannel: (...args: any[]) => mockUseUploaderChannel(...args),\n}))\nvi.mock('../../src/hooks/useUploaderConnections', () => ({ useUploaderConnections: () => [] }))\nvi.mock('react-qr-code', () => ({ default: () => <div>QR</div> }))\nvi.mock('../../src/components/CopyableInput', () => ({ CopyableInput: () => <div>Input</div> }))\nvi.mock('../../src/components/ConnectionListItem', () => ({ ConnectionListItem: () => <div>Item</div> }))\nvi.mock('../../src/components/StopButton', () => ({ default: () => <button>Stop</button> }))\n\nimport Uploader from '../../src/components/Uploader'\n\ndescribe('Uploader', () => {\n  it('shows loading when channel loading', () => {\n    mockUseUploaderChannel = vi.fn().mockReturnValueOnce({\n      isLoading: true,\n      error: null,\n      longSlug: undefined,\n      shortSlug: undefined,\n      longURL: undefined,\n      shortURL: undefined,\n    })\n    const { getByText } = render(<Uploader files={[]} password=\"\" onStop={() => {}} />)\n    expect(getByText('Creating channel...')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/WebRTCProvider.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render, waitFor } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\n\nvi.stubGlobal('fetch', vi.fn(() => Promise.resolve(new Response(JSON.stringify({ iceServers: [] })))) )\nvi.mock('peerjs', () => ({\n  default: class { id = 'peer1'; on(event: string, cb: (id: string) => void) { if (event === 'open') cb('peer1') } off() {} },\n}))\n\nimport WebRTCProvider from '../../src/components/WebRTCProvider'\n\nconst Child = () => <div>child</div>\n\ndescribe('WebRTCProvider', () => {\n  it('renders children after init', async () => {\n    const { getByText } = render(\n      <WebRTCProvider>\n        <Child />\n      </WebRTCProvider>,\n    )\n    await waitFor(() => expect(getByText('child')).toBeInTheDocument())\n  })\n})\n"
  },
  {
    "path": "tests/unit/Wordmark.test.tsx",
    "content": "/// <reference types=\"@testing-library/jest-dom\" />\nimport React from 'react'\nimport { render } from '@testing-library/react'\nimport { describe, it, expect } from 'vitest'\nimport Wordmark from '../../src/components/Wordmark'\n\ndescribe('Wordmark', () => {\n  it('renders svg', () => {\n    const { getByLabelText } = render(<Wordmark />)\n    expect(getByLabelText('FilePizza logo')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "tests/unit/isFinalChunk.test.ts",
    "content": "import { describe, it, expect } from 'vitest'\nimport { isFinalChunk, MAX_CHUNK_SIZE } from '../../src/hooks/useUploaderConnections'\n\ndescribe('isFinalChunk', () => {\n  it('marks last chunk when file size is exact multiple of chunk size', () => {\n    const fileSize = MAX_CHUNK_SIZE * 2\n    // when offset points to start of last chunk\n    expect(isFinalChunk(MAX_CHUNK_SIZE, fileSize)).toBe(true)\n  })\n\n  it('returns false for middle chunks', () => {\n    const fileSize = MAX_CHUNK_SIZE * 3 + 123\n    expect(isFinalChunk(MAX_CHUNK_SIZE, fileSize)).toBe(false)\n  })\n})\n"
  },
  {
    "path": "tests/unit/useRotatingSpinner.test.ts",
    "content": "import { describe, it, expect, vi } from 'vitest'\nimport {\n  setRotating,\n  addRotationListener,\n  removeRotationListener,\n  getRotating,\n} from '../../src/hooks/useRotatingSpinner'\n\ndescribe('useRotatingSpinner state helpers', () => {\n  it('notifies listeners on state change', () => {\n    const listener = vi.fn()\n    addRotationListener(listener)\n    setRotating(true)\n    expect(listener).toHaveBeenCalledWith(true)\n    expect(getRotating()).toBe(true)\n    setRotating(false)\n    expect(listener).toHaveBeenCalledWith(false)\n    expect(getRotating()).toBe(false)\n    removeRotationListener(listener)\n  })\n})\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": false,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"downlevelIteration\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"strictNullChecks\": true,\n    \"types\": [\n      \"vitest/globals\",\n      \"@testing-library/jest-dom\"\n    ]\n  },\n  \"include\": [\n    \"tailwind.config.js\",\n    \"next-env.d.ts\",\n    \"src/**/*.js\",\n    \"src/**/*.ts\",\n    \"src/**/*.tsx\",\n    \"tests/**/*.ts\",\n    \"tests/**/*.tsx\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  plugins: [react()],\n  test: {\n    environment: 'jsdom',\n    globals: true,\n    setupFiles: './vitest.setup.ts',\n    exclude: ['tests/e2e/**', '**/node_modules/**'],\n    coverage: {\n      provider: 'v8',\n      reporter: ['text', 'html'],\n    },\n  },\n})\n"
  },
  {
    "path": "vitest.setup.ts",
    "content": "import '@testing-library/jest-dom'\n"
  }
]