[
  {
    "path": ".github/workflows/main.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \".github/workflows/main.yml\"\n      - \"app/**/*.ts\"\n      - \"app/**/*.tsx\"\n      - \"public/*\"\n      - \"styles/*\"\n      - \"worker/*\"\n      - \"tests/*\"\n      - \"package.json\"\n      - \"package-lock.json\"\n      - \"remix.config.js\"\n      - \"tsconfig.json\"\n      - \"wrangler.toml\"\n      - \"remix.env.d.ts\"\n      - \"tailwind.config.js\"\n\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: \"20\"\n          registry-url: \"https://registry.npmjs.org\"\n      - run: npm ci\n      - run: npm test\n      - run: npm run build\n      - name: Publish app\n        uses: cloudflare/wrangler-action@1.3.0\n        with:\n          apiToken: ${{ secrets.CF_API_TOKEN }}\n          environment: \"production\"\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n\n/.cache\n/build\n/public/build\n.env\n/app/tailwind.css\n/jsonDocs\n.DS_Store\n/dist\n.mf\n/meta.json\n/stats.html\npublic/entry.worker.js"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"pwa-chrome\",\n      \"request\": \"launch\",\n      \"name\": \"Launch Chrome against localhost with document\",\n      \"url\": \"http://localhost:8787\",\n      \"webRoot\": \"${workspaceFolder}/app\"\n    },\n    {\n      \"name\": \"Debug Jest All Tests\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"runtimeArgs\": [\n        \"--inspect-brk\",\n        \"${workspaceRoot}/node_modules/.bin/jest\",\n        \"--runInBand\"\n      ],\n      \"console\": \"integratedTerminal\",\n      \"internalConsoleOptions\": \"neverOpen\"\n    },\n    {\n      \"name\": \"Debug Jest Test File\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"runtimeArgs\": [\n        \"--inspect-brk\",\n        \"${workspaceRoot}/node_modules/.bin/jest\",\n        \"--runInBand\"\n      ],\n      \"args\": [\"${fileBasename}\", \"--no-cache\"],\n      \"console\": \"integratedTerminal\",\n      \"internalConsoleOptions\": \"neverOpen\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"type\": \"typescript\",\n      \"tsconfig\": \"tsconfig.json\",\n      \"option\": \"watch\",\n      \"problemMatcher\": [\"$tsc-watch\"],\n      \"group\": \"build\",\n      \"label\": \"tsc: watch - tsconfig.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## ⚡️ JSON Hero Contributing Guide\n\nFirst of all, thanks for considering contributing to this project! If you have any questions please don't hesitate to reach out to [eric@jsonhero.io](mailto:eric@jsonhero.io) or join us on [Discord](https://discord.gg/JtBAxBr2m3).\n\nJSON Hero is a Typescript React application built with [remix.run](https://remix.run), with support for deploying to Cloudflare workers.\n\nTo get started with contributing, please read our [Development guide](https://github.com/triggerdotdev/jsonhero-web/blob/main/DEVELOPMENT.md) first to get JSON Hero running locally.\n\n### Running tests\n\nAlthough there is less test-coverage for JSON Hero than there should be, tests should still be run to ensure builds have not been broken:\n\n```bash\nnpm test\n```\n\nYou can also run tests in \"watch\" mode:\n\n```bash\nnpm run test:watch\n```\n\n### Making changes\n\nPlease make any changes to your forked repository in a branch other than `main`. If you are working on a bug fix, please use the `bug/` prefix for your branch name. If you are working on a feature, please use `features/`. If you are working on a specific issue please name the branch `issue-<issue number>`\n\nMake sure to run the `npm lint` command to ensure there are no Typescript compile-time errors.\n\n### Pull Requests\n\nPlease open a Pull Request against the `main` branch in the `triggerdotdev/jsonhero-web` repository. We will aim to address all newly opened PRs by the following Friday. If you haven't opened a Pull Request before, please check out GitHub's [Pull Request documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests)\n\n### Other JSON Hero projects\n\nIf you'd like to contribute to the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode), please see the [triggerdotdev/vscode-extension](https://github.com/triggerdotdev/vscode-extension) repo.\n\nFor issues related to the JSON Schema inference, please check out [triggerdotdev/schema-infer](https://github.com/triggerdotdev/schema-infer).\n\nThe \"Smart Preview\" feature is in-part powered by the [@jsonhero/json-infer-types](https://github.com/triggerdotdev/json-infer-types) project.\n\nIf it's related to the Search functionality, please see the [triggerdotdev/fuzzy-json-search](https://github.com/triggerdotdev/fuzzy-json-search) repo.\n"
  },
  {
    "path": "DEVELOPMENT.md",
    "content": "## 👩🏽‍💻 JSON Hero Local Development Guide\n\nWelcome to JSON Hero development and thanks for being here! If you'd like to run JSON Hero locally, please use the following guide to get started. If you have any issues with this guide please feel free to email me at [eric@jsonhero.io](mailto:eric@jsonhero.io) or come leave a message in our open [Discord Channel](https://discord.gg/JtBAxBr2m3).\n\nFor more information about contributing to JSON Hero please see the [Contributing doc](https://github.com/triggerdotdev/jsonhero-web/blob/main/CONTRIBUTING.md).\n\n### Install dependencies\n\nBefore you can run JSON Hero locally, you will need to install the following dependencies on your machine:\n\n#### Git\n\nYou most likely already have git installed on your machine, but if not, you can install it from the [Git website](https://git-scm.com).\n\n#### Node.js 16\n\nEven though JSON Hero runs on [Cloudflare Workers](https://workers.cloudflare.com), which isn't a Node.js environment, you will still need Node.js 16 to run it locally. The recommended way to install Node.js is to download a pre-built package from the [Node.js website](https://nodejs.org/en/)\n\n#### NPM\n\nIf you install Node.js through the above link, you should also have NPM automatically installed as well. To make sure, run the following command in your preferred Terminal:\n\n```bash\nnpm ---version\n```\n\n### Fork JSON Hero on GitHub (optional)\n\nTo contribute code to JSON Hero, you should first create a fork of the [jsonhero-web](https://github.com/triggerdotdev/jsonhero-web) repository on GitHub. Follow [these instructions](https://docs.github.com/en/get-started/quickstart/fork-a-repo) on repository forking.\n\n### Clone the repo\n\nIn your terminal, issue the following command to clone the repository to your local machine:\n\n```bash\ngit clone https://github.com/triggerdotdev/jsonhero-web.git\n```\n\nOr if you've forked the repository:\n\n```bash\ngit clone https://github.com/<github username>/jsonhero-web.git\n```\n\nThen `cd` into the repository:\n\n```bash\ncd jsonhero-web\n```\n\n### Prepare the repo\n\nFirst, install npm dependencies:\n\n```bash\nnpm install\n```\n\nRun the following command to create the `.env` file with a new `SESSION_SECRET` environment variable:\n\n```bash\necho \"SESSION_SECRET=$(openssl rand -hex 32)\" > .env\n```\n\nThen, run `npm run build` or `npm run dev` to build.\n\nStart the development server:\n\n```bash\nnpm start\n```\n\nYou should now be able to access your local JSON Hero server on [localhost:8787](http://localhost:8787)\n\n> **Note** JSON documents created locally are not persisted across server restarts\n\n### Previewing URLs\n\nWe currently use [OpenGraph Ninja](https://opengraph.ninja/) to power some of the Preview URL functionality.\n\n### Deploying to Cloudflare\n\n_Coming Soon_\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Builder\nFROM node:16.17.0 as builder\nWORKDIR /src\nCOPY . /src\n\n# App\nRUN cd /src\nRUN npm install\nRUN echo \"SESSION_SECRET=abc123\" > .env\nRUN npm run build\n\nCMD npm start\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/4a157bda-2a99-4ac3-6bc7-be08b4a46600/public\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/31447544-b16f-49dd-c206-74b1802c6700/public\">\n  <img width=200 alt=\"Trigger.dev logo\" src=\"https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/4a157bda-2a99-4ac3-6bc7-be08b4a46600/public\">\n</picture>\n</div>\n\n</br>\n<p align=\"center\">\n  <a href=\"https://console.algora.io/org/triggerdotdev/bounties?status=open\"><img src=\"https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Ftriggerdotdev%2Fbounties%3Fstatus%3Dopen\" alt=\"Open Bounties\" /></a>\n  <a href=\"https://console.algora.io/org/triggerdotdev/bounties?status=completed\"><img src=\"https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Ftriggerdotdev%2Fbounties%3Fstatus%3Dcompleted\" alt=\"Rewarded Bounties\" /></a>\n</p>\n\n# Brought to you by Trigger.dev\n\nJSON Hero was created and is maintained by the team behind [Trigger.dev](https://trigger.dev). With Trigger.dev you can trigger workflows from APIs, on a schedule, or on demand. We make API calls easy with authentication handled for you, and you can add durable delays that survive server restarts.\n\n# JSON Hero\n\nJSON Hero makes reading and understand JSON files easy by giving you a clean and beautiful UI packed with extra features.\n\n- View JSON any way you'd like: Column View, Tree View, Editor View, and more.\n- Automatically infers the contents of strings and provides useful previews\n- Creates an inferred JSON Schema that could be used to validate your JSON\n- Quickly scan related values to check for edge cases\n- Search your JSON files (both keys and values)\n- Keyboard accessible\n- Easily sharable URLs with path support\n\n![JSON Hero Screenshot](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/0f5735b3-2421-470b-244c-7047fd77f700/public)\n\n## Features\n\n### Send to JSON Hero\n\nSend your JSON to JSON Hero in a variety of ways\n\n- Head to [jsonhero.io](https://jsonhero.io) and Drag and Drop a JSON file, or paste JSON or a JSON url in the provided form\n- Include a Base64 encoded string of a JSON payload: [jsonhero.io/new?j=eyAiZm9vIjogImJhciIgfQ==](https://jsonhero.io/new?j=eyAiZm9vIjogImJhciIgfQ==)\n- Include a JSON URL to the `new` endpoint: [jsonhero.io/new?url=https://jsonplaceholder.typicode.com/todos/1](https://jsonhero.io/new?url=https://jsonplaceholder.typicode.com/todos/1)\n- Install the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode) and open JSON from VS Code\n- Raycast user? Check out our extension [here](https://www.raycast.com/maverickdotdev/open-in-json-hero)\n- Use the unofficial API:\n\n  - Make a `POST` request to `jsonhero.io/api/create.json` with the following JSON body:\n\n  ```json\n  {\n    \"title\": \"test 123\",\n    \"content\": { \"foo\": \"bar\" },\n    \"readOnly\": false, // this is optional, will make it so the document title cannot be edited or document cannot be deleted\n    \"ttl\": 3600 // this will expire the document after 3600 seconds, also optional\n  }\n  ```\n\n  The JSON response will be the following:\n\n  ```json\n  {\n    \"id\": \"YKKduNySH7Ub\",\n    \"title\": \"test 123\",\n    \"location\": \"https://jsonhero.io/j/YKKduNySH7Ub\"\n  }\n  ```\n\n### Column view\n\nInspired by macOS Finder, Column View is a new way to browse a JSON document.\n\n![JSON Hero Column View](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-columnview.gif)\n\nIt has all the features you'd expect: Keyboard navigation, Path bar, history.\n\nIt also has a nifty feature that allows you to \"hold\" a descendent selected and travel up through the hierarchy, and then move between siblings and view the different values found at that path. It's hard to describe, but here is an animation to help demonstrate:\n\n![Column View - Traverse with Context](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-traversewithcontext.gif)\n\nAs you can see, holding the `Option` (or `Alt` key on Windows) while moving to a parent keeps the part of the document selected and shows it in context of it's surrounding JSON. Then you can traverse between items in an array and compare the values of the selection across deep hierarchy cahnges.\n\n### Editor view\n\nView your entire JSON document in an editor, but keep the nice previews and related values you get from the sidebar as you move around the document:\n\n![Editor view](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-editorview.gif)\n\n### Tree view\n\nUse a traditional tree view to traverse your JSON document, with collapsible sections and keyboard shortcuts. All while keeping the nice previews:\n\n![Tree view](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-treeview.gif)\n\n### Search\n\nQuickly open a search panel and fuzzy search your entire JSON file in milliseconds. Searches through key names, key paths, values, and even pretty formatted values (e.g. Searching for `\"Dec\"` will find datetime strings in the month of December.)\n\n![Search](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-search.gif)\n\n### Content Previews\n\nJSON Hero automatically infers the content of strings and provides useful previews and properties of the value you've selected. It's \"Show Don't Tell\" for JSON:\n\n#### Dates and Times\n\n![Preview colors](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/43f2c081-c09b-47db-cb10-8f15ee6a1a00/public)\n\n#### Image URLs\n\n![Preview colors](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/8a743bd5-a065-4f7f-1262-585c39c10100/public)\n\n#### Website URLs\n\n![Preview websites](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/cd7f2d28-2c8d-4b37-696d-e898937c3d00/public)\n\n#### Tweet URLS\n\n![Preview tweets](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/8455e9d6-1d3e-451e-a032-f3259204ef00/public)\n\n#### JSON URLs\n\n![Preview JSON](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/13743860-3d9c-4cac-dde9-881fba7eba00/public)\n\n#### Colors\n\n![Preview colors](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/22e37599-c2bd-4abd-79f2-466241d17b00/public)\n\n### Related Values\n\nEasily see all the related values across your entire JSON document for a specific field, including any `undefined` or `null` values.\n\n![Editor view](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-relatedvalues.gif)\n\n<!-- TODO -->\n\n## Bugs and Feature Requests\n\nHave a bug or a feature request? Feel free to [open a new issue](https://github.com/triggerdotdev/jsonhero-web/issues).\n\nYou can also join our [Discord channel](https://discord.gg/JtBAxBr2m3) to hang out and discuss anything you'd like.\n\n## Developing\n\nTo run locally, first clone the repo and install the dependencies:\n\n```bash\ngit clone https://github.com/triggerdotdev/jsonhero-web.git\ncd jsonhero-web\nnpm install\n```\n\nThen, create a file at the root of the repo called `.env` and set the `SESSION_SECRET` value:\n\n```\nSESSION_SECRET=abc123\n```\n\nThen, run `npm run build` or `npm run dev` to build.\n\nNow, run `npm start` and open your browser to `http://localhost:8787`\n"
  },
  {
    "path": "SELF_HOSTING.md",
    "content": "## Deploying to Cloudflare\n\n### Install and login to wrangler\n```bash\nnpm install -g wrangler\nwrangler login\n```\n\n### Create service\nGo to workers tab from your [cloudflare profile](https://dash.cloudflare.com/profile) and create a new worker. Use HTTP Handler as service type. The name of worker must match the `name` field in `wrangler.toml`.\n\n### Setup wrangler.toml\nEdit the following variables in `wrangler.toml` and `wrangler.toml.dev`:\n- `account_id`: Get account id by using\n    ```bash\n    wrangler whoami\n    ```\n- `kv_namespaces`: Run the following comands to create a new KV namespace.\n    ```bash\n    wrangler kv:namespace create DOCUMENTS # gives namespace id\n    wrangler kv:namespace create DOCUMENTS --preview # gives preview id for namespace\n    ```\n    Replace current entry for `kv_namespaces` as:\n    ```toml\n    kv_namespaces = [\n    { binding = \"DOCUMENTS\", id = <YOUR_ID>, preview_id = <YOUR_PREVIEW_ID> }\n    ]\n    ```\n\n### Configure Environment Variables\nSet `SESSION_SECRET` environment for worker.\n```bash\nwrangler secret put SESSION_SECRET\n```\nOptionally set other secrets listed at the end of `wrangler.toml`.\n\n### Publish worker\n```bash\nwrangler publish\n```\n"
  },
  {
    "path": "app/bindings.d.ts",
    "content": "export {};\n\ndeclare global {\n  const DOCUMENTS: KVNamespace;\n  const SESSION_SECRET: string;\n  const GRAPH_JSON_API_KEY: string;\n  const GRAPH_JSON_COLLECTION: string;\n  const APIHERO_PROJECT_KEY: string;\n}\n"
  },
  {
    "path": "app/components/AutoplayVideo.tsx",
    "content": "import { useEffect, useRef } from \"react\";\nimport { useOnScreen } from \"~/hooks/useOnScreen\";\n\nexport function AutoplayVideo({ src }: { src: string }) {\n  const elementRef = useRef<HTMLVideoElement>(null);\n  const isOnScreen = useOnScreen(elementRef);\n\n  useEffect(() => {\n    if (elementRef.current == null) return;\n\n    elementRef.current.muted = true;\n    elementRef.current.playsInline = true;\n\n    if (isOnScreen) {\n      elementRef.current.play();\n    } else {\n      elementRef.current.pause();\n    }\n  }, [isOnScreen]);\n\n  return (\n    <video\n      src={src}\n      ref={elementRef}\n      loop={true}\n      muted={true}\n      autoPlay={false}\n    />\n  );\n}\n"
  },
  {
    "path": "app/components/BlankColumn.tsx",
    "content": "import { memo } from \"react\";\n\nfunction BlankColumnElement() {\n  return (\n    <div\n      className={\n        \"column flex-none border-r-[1px] border-slate-300 w-80 transition dark:border-slate-600\"\n      }\n    ></div>\n  );\n}\n\nexport const BlankColumn = memo(BlankColumnElement);\n"
  },
  {
    "path": "app/components/CodeEditor.tsx",
    "content": "import { json as jsonLang } from \"@codemirror/lang-json\";\nimport {\n  EditorView,\n  TransactionSpec,\n  useCodeMirror,\n  ViewUpdate,\n} from \"@uiw/react-codemirror\";\nimport { useRef, useEffect } from \"react\";\nimport { useJsonDoc } from \"~/hooks/useJsonDoc\";\nimport { getEditorSetup } from \"~/utilities/codeMirrorSetup\";\nimport { darkTheme, lightTheme } from \"~/utilities/codeMirrorTheme\";\nimport { useTheme } from \"./ThemeProvider\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\n\nexport type CodeEditorProps = {\n  content: string;\n  language?: \"json\";\n  readOnly?: boolean;\n  onChange?: (value: string) => void;\n  onUpdate?: (update: ViewUpdate) => void;\n  selection?: { start: number; end: number };\n};\n\nconst languages = {\n  json: jsonLang,\n};\n\ntype CodeEditorDefaultProps = Required<\n  Omit<CodeEditorProps, \"content\" | \"onChange\" | \"onUpdate\">\n>;\n\nconst defaultProps: CodeEditorDefaultProps = {\n  language: \"json\",\n  readOnly: true,\n  selection: { start: 0, end: 0 },\n};\n\nexport function CodeEditor(opts: CodeEditorProps) {\n  const { content, language, readOnly, onChange, onUpdate, selection } = {\n    ...defaultProps,\n    ...opts,\n  };\n\n  const [theme] = useTheme();\n\n  const extensions = getEditorSetup();\n\n  const languageExtension = languages[language];\n\n  extensions.push(languageExtension());\n\n  const editor = useRef(null);\n  const { setContainer, view, state } = useCodeMirror({\n    container: editor.current,\n    extensions,\n    editable: !readOnly,\n    contentEditable: !readOnly,\n    value: content,\n    autoFocus: false,\n    theme: theme === \"light\" ? lightTheme() : darkTheme(),\n    indentWithTab: false,\n    basicSetup: false,\n    onChange,\n    onUpdate,\n  });\n\n  useEffect(() => {\n    if (editor.current) {\n      setContainer(editor.current);\n    }\n  }, [editor.current]);\n\n  const setSelectionRef = useRef(false);\n\n  useEffect(() => {\n    if (setSelectionRef.current) {\n      return;\n    }\n\n    if (view) {\n      setSelectionRef.current = true;\n\n      const selectionStart = selection?.start ?? defaultProps.selection.start;\n      const selectionEnd = selection?.end ?? defaultProps.selection.end;\n\n      const lineNumber = state?.doc.lineAt(selectionStart).number;\n\n      const transactionSpec: TransactionSpec = {\n        selection: { anchor: selectionStart, head: selectionEnd },\n        effects: EditorView.scrollIntoView(selectionStart, {\n          y: \"start\",\n          yMargin: 100,\n        }),\n      };\n\n      view.dispatch(transactionSpec);\n    }\n  }, [selection, view, setSelectionRef.current]);\n\n  const { minimal } = useJsonDoc();\n\n  useHotkeys(\n   \"ctrl+a,meta+a,command+a\",\n   (e) => {\n     e.preventDefault();\n     view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });\n   },\n   [view, state]\n );\n\n\n  return (\n    <div>\n      <div\n        className={`${\n          minimal ? \"h-jsonViewerHeightMinimal\" : \"h-jsonViewerHeight\"\n        } overflow-y-auto no-scrollbar`}\n        ref={editor}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/CodeViewer.tsx",
    "content": "import { json as jsonLang } from \"@codemirror/lang-json\";\nimport { useCodeMirror } from \"@uiw/react-codemirror\";\nimport { useRef, useEffect } from \"react\";\nimport { getViewerSetup } from \"~/utilities/codeMirrorSetup\";\nimport { darkTheme, lightTheme } from \"~/utilities/codeMirrorTheme\";\nimport { useTheme } from \"./ThemeProvider\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\n\nexport function CodeViewer({ code, lang }: { code: string; lang?: \"json\" }) {\n  const editor = useRef(null);\n\n  const extensions = getViewerSetup();\n\n  if (!lang || lang === \"json\") {\n    extensions.push(jsonLang());\n  }\n\n  const [theme] = useTheme();\n\n  const { setContainer, view, state } = useCodeMirror({\n    container: editor.current,\n    extensions,\n    value: code,\n    editable: false,\n    contentEditable: false,\n    autoFocus: false,\n    basicSetup: false,\n    theme: theme === \"light\" ? lightTheme() : darkTheme(),\n  });\n\n  useEffect(() => {\n    if (editor.current) {\n      setContainer(editor.current);\n    }\n  }, [editor.current]);\n\n  useHotkeys(\n    \"ctrl+a,meta+a,command+a\",\n    (e) => {\n      e.preventDefault();\n      view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });\n    },\n    [view, state]\n  );\n\n  return (\n    <div>\n      <div ref={editor} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Column.tsx",
    "content": "import { Title } from \"./Primitives/Title\";\nimport { colorForItemAtPath } from \"~/utilities/colors\";\nimport { IconComponent } from \"~/useColumnView\";\nimport { useJson } from \"../hooks/useJson\";\nimport { memo, useMemo } from \"react\";\nimport { useJsonDoc } from \"~/hooks/useJsonDoc\";\n\nexport type ColumnProps = {\n  id: string;\n  title: string;\n  icon?: IconComponent;\n  hasHighlightedElement: boolean;\n  children: React.ReactNode;\n};\n\nfunction ColumnElement(column: ColumnProps) {\n  const { id, title, children } = column;\n  const [json] = useJson();\n  const { minimal } = useJsonDoc();\n  const iconColor = useMemo(() => colorForItemAtPath(id, json), [id, json]);\n\n  return (\n    <div\n      className={\n        \"column flex-none border-r-[1px] border-slate-300 w-80 transition dark:border-slate-600\"\n      }\n    >\n      <div className=\"flex items-center text-slate-800 bg-slate-50 mb-[3px] p-2 pb-0 transition dark:bg-slate-900 dark:text-slate-300\">\n        {column.icon && <column.icon className=\"h-6 w-6 mr-1\" />}\n        <Title className=\"text-ellipsis overflow-hidden\">{title}</Title>\n      </div>\n      <div\n        className={`overflow-y-auto ${\n          minimal ? \"h-viewerHeightMinimal\" : \"h-viewerHeight\"\n        } no-scrollbar`}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport const Column = memo(ColumnElement);\n"
  },
  {
    "path": "app/components/ColumnItem.tsx",
    "content": "import { ChevronRightIcon } from \"@heroicons/react/outline\";\nimport { Mono } from \"./Primitives/Mono\";\nimport { memo, useEffect, useMemo, useRef } from \"react\";\nimport { ColumnViewNode } from \"~/useColumnView\";\nimport { colorForItemAtPath } from \"~/utilities/colors\";\nimport { Body } from \"./Primitives/Body\";\n\nexport type ColumnItemProps = {\n  item: ColumnViewNode;\n  json: unknown;\n  isSelected: boolean;\n  isHighlighted: boolean;\n  onClick?: (id: string) => void;\n};\n\nfunction ColumnItemElement({\n  item,\n  json,\n  isSelected,\n  isHighlighted,\n  onClick,\n}: ColumnItemProps) {\n  const htmlElement = useRef<HTMLDivElement>(null);\n\n  const showArrow = item.children.length > 0;\n\n  const stateStyle = useMemo<string>(() => {\n    if (isHighlighted) {\n      return \"bg-slate-300 text-slate-700 hover:bg-slate-400 hover:bg-opacity-60 transition duration-75 ease-out dark:bg-white dark:bg-opacity-[15%] dark:text-slate-100\";\n    }\n\n    if (isSelected) {\n      return \"bg-slate-200 hover:bg-slate-300 transition duration-75 ease-out dark:bg-white dark:bg-opacity-[5%] dark:hover:bg-white dark:hover:bg-opacity-[10%] dark:text-slate-200\";\n    }\n\n    return \"hover:bg-slate-100 transition duration-75 ease-out dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:text-slate-400\";\n  }, [isSelected, isHighlighted]);\n\n  const iconColor = useMemo<string>(\n    () => colorForItemAtPath(item.id, json),\n    [item.id, json]\n  );\n\n  useEffect(() => {\n    if (isSelected || isHighlighted) {\n      htmlElement.current?.scrollIntoView({\n        block: \"nearest\",\n        inline: \"center\",\n      });\n    }\n  }, [isSelected, isHighlighted]);\n\n  return (\n    <div\n      className={`flex h-9 items-center justify-items-stretch mx-1 px-1 py-1 my-1 rounded-sm ${stateStyle}`}\n      onClick={() => onClick && onClick(item.id)}\n      ref={htmlElement}\n    >\n      <div className=\"w-4 flex-none flex-col justify-items-center\">\n        {item.icon && (\n          <item.icon\n            className={`h-5 w-5 ${\n              isSelected && isHighlighted\n                ? \"text-slate-900 dark:text-slate-300\"\n                : \"text-slate-500\"\n            }`}\n          />\n        )}\n      </div>\n\n      <div className=\"flex flex-grow flex-shrink items-baseline justify-between truncate\">\n        <Body className=\"flex-grow flex-shrink-0 pl-3 pr-2 \">{item.title}</Body>\n        {item.subtitle && (\n          <Mono\n            className={`truncate pr-1 transition duration-75 ${\n              isHighlighted\n                ? \"text-gray-500 dark:text-slate-100\"\n                : \"text-gray-400 dark:text-gray-500\"\n            }`}\n          >\n            {item.subtitle}\n          </Mono>\n        )}\n      </div>\n\n      {showArrow && (\n        <ChevronRightIcon className=\"flex-none w-4 h-4 text-gray-400\" />\n      )}\n    </div>\n  );\n}\n\nexport const ColumnItem = memo(ColumnItemElement);\n"
  },
  {
    "path": "app/components/Columns.tsx",
    "content": "import { JSONHeroPath } from \"@jsonhero/path\";\nimport { memo, useMemo } from \"react\";\nimport { useJson } from \"~/hooks/useJson\";\nimport {\n  useJsonColumnViewAPI,\n  useJsonColumnViewState,\n} from \"~/hooks/useJsonColumnView\";\nimport { ColumnDefinition } from \"~/useColumnView\";\nimport { BlankColumn } from \"./BlankColumn\";\nimport { Column } from \"./Column\";\nimport { ColumnItem } from \"./ColumnItem\";\n\nfunction ColumnsElement({ columns }: { columns: ColumnDefinition[] }) {\n  const [json] = useJson();\n  const { selectedPath, highlightedPath, highlightedNodeId } =\n    useJsonColumnViewState();\n  const { goToNodeId } = useJsonColumnViewAPI();\n  const highlightedItemIsValue = useMemo<boolean>(() => {\n    if (highlightedNodeId == null) {\n      return false;\n    }\n\n    const path = new JSONHeroPath(highlightedNodeId);\n    let item = path.first(json);\n\n    return typeof item !== \"object\";\n  }, [highlightedPath, json]);\n\n  return (\n    <div className=\"columns flex flex-grow overflow-x-auto focus:outline-none no-scrollbar\">\n      {columns.map((column) => {\n        return (\n          <Column\n            key={column.id}\n            id={column.id}\n            title={column.title}\n            icon={column.icon}\n            hasHighlightedElement={\n              highlightedPath[highlightedPath.length - 2] === column.id\n            }\n          >\n            {column.items.map((item) => (\n              <ColumnItem\n                key={item.id}\n                item={item}\n                json={json}\n                isSelected={selectedPath.includes(item.id)}\n                isHighlighted={\n                  highlightedPath[highlightedPath.length - 1] === item.id\n                }\n                onClick={(id) => goToNodeId(id, \"columnView\")}\n              />\n            ))}\n          </Column>\n        );\n      })}\n      {highlightedItemIsValue ? <BlankColumn /> : null}\n    </div>\n  );\n}\nexport const Columns = memo(ColumnsElement);\n"
  },
  {
    "path": "app/components/ContainerInfo.tsx",
    "content": "import { inferType } from \"@jsonhero/json-infer-types\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\nimport { useJson } from \"~/hooks/useJson\";\nimport { useJsonColumnViewState } from \"~/hooks/useJsonColumnView\";\nimport { pathToDescendant } from \"~/utilities/jsonColumnView\";\nimport { JsonPreview } from \"./JsonPreview\";\nimport { JsonSchemaViewer } from \"./JsonSchemaViewer\";\nimport { TabContent, Tabs } from \"./UI/Tabs\";\n\nconst tabs = [\n  { value: \"json\", label: \"JSON\" },\n  { value: \"schema\", label: \"Schema\" },\n];\n\nexport function ContainerInfo() {\n  const { selectedNodeId, highlightedNodeId } = useJsonColumnViewState();\n\n  if (!selectedNodeId || !highlightedNodeId) {\n    return <></>;\n  }\n\n  const [json] = useJson();\n\n  const selectedHeroPath = new JSONHeroPath(selectedNodeId);\n  const selectedJson = selectedHeroPath.first(json);\n  const selectedInfo = inferType(selectedJson);\n\n  const isSelectedLeafNode =\n    selectedInfo.name !== \"object\" && selectedInfo.name !== \"array\";\n\n  const highlightedHeroPath = new JSONHeroPath(highlightedNodeId);\n  const highlightedJson = highlightedHeroPath.first(json);\n  const highlightedInfo = inferType(highlightedJson);\n\n  const isHighlightedLeafNode =\n    highlightedInfo.name !== \"object\" && highlightedInfo.name !== \"array\";\n\n  const shouldHighlightInPreview =\n    selectedNodeId !== highlightedNodeId && !isHighlightedLeafNode;\n\n  const shouldDisplayCodePreview =\n    shouldHighlightInPreview || !isSelectedLeafNode;\n\n  if (!shouldDisplayCodePreview) {\n    return <></>;\n  }\n\n  return (\n    <Tabs tabs={tabs}>\n      <>\n        <TabContent value=\"json\">\n          {shouldHighlightInPreview ? (\n            <JsonPreview\n              json={highlightedJson}\n              highlightPath={pathToDescendant(\n                highlightedNodeId,\n                selectedNodeId\n              )}\n            />\n          ) : (\n            <JsonPreview json={selectedJson} />\n          )}\n        </TabContent>\n        <TabContent value=\"schema\">\n          {shouldHighlightInPreview ? (\n            <JsonSchemaViewer path={highlightedNodeId} />\n          ) : (\n            <JsonSchemaViewer path={selectedNodeId} />\n          )}\n        </TabContent>\n      </>\n    </Tabs>\n  );\n}\n"
  },
  {
    "path": "app/components/CopySelectedNode.tsx",
    "content": "import { useHotkeys } from \"react-hotkeys-hook\";\nimport { useSelectedInfo } from \"../hooks/useSelectedInfo\";\n\nexport function CopySelectedNodeShortcut() {\n  const selectedInfo = useSelectedInfo();\n\n  useHotkeys(\n    'shift+c,shift+C',\n    (e) => {\n      e.preventDefault();\n      const selectedJSON = selectedInfo?.name === \"string\"\n        ? selectedInfo?.value\n        : JSON.stringify(selectedInfo?.value, null, 2);\n      navigator.clipboard.writeText(selectedJSON);\n    },\n    [selectedInfo]\n  );\n\n  return <></>;\n}\n"
  },
  {
    "path": "app/components/CopyText.tsx",
    "content": "import React, { useCallback } from \"react\";\n\nexport type CopyTextProps = {\n  children?: React.ReactNode;\n  value: string;\n  className?: string;\n  onCopied?: () => void;\n};\n\nexport function CopyText({\n  children,\n  value,\n  className,\n  onCopied,\n}: CopyTextProps) {\n  const onClick = useCallback(() => {\n    navigator.clipboard.writeText(value);\n    if (onCopied) {\n      onCopied();\n    }\n  }, [value]);\n\n  return (\n    <div onClick={onClick} className={`${className}`}>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/CopyTextButton.tsx",
    "content": "import { ClipboardIcon } from \"@heroicons/react/outline\";\nimport { useCallback, useState } from \"react\";\nimport { CopyText } from \"./CopyText\";\nimport { Body } from \"./Primitives/Body\";\n\nexport type CopyTextButtonProps = {\n  value: string;\n  className?: string;\n};\n\nexport function CopyTextButton({ value, className }: CopyTextButtonProps) {\n  const [copied, setCopied] = useState(false);\n  const onCopied = useCallback(() => {\n    setCopied(true);\n    const timeout = setTimeout(() => {\n      setCopied(false);\n    }, 1500);\n  }, [value]);\n  return (\n    <CopyText className={`${className}`} value={value} onCopied={onCopied}>\n      {copied ? (\n        <Body>Copied!</Body>\n      ) : (\n        <div className=\"flex items-center\">\n          <ClipboardIcon className=\"h-4 w-4 mr-[2px]\" />\n          <Body>Copy</Body>\n        </div>\n      )}\n    </CopyText>\n  );\n}\n"
  },
  {
    "path": "app/components/DataTable.tsx",
    "content": "import { FunctionComponent, useState } from \"react\";\nimport { CopyTextButton } from \"./CopyTextButton\";\nimport { Title } from \"./Primitives/Title\";\n\nexport type DataTableProps = {\n  rows: DataTableRow[];\n};\n\nexport type DataTableRow = {\n  key: string;\n  value: string;\n  icon?: JSX.Element;\n};\n\ntype DataRowProps = {\n  title: string;\n  value: string;\n  icon?: JSX.Element;\n};\n\nconst DataRow: FunctionComponent<DataRowProps> = ({ title, value, icon }) => {\n  const [hovering, setHovering] = useState(false);\n  return (\n    <tr className=\"divide-solid divide-x transition dark:divide-slate-700\">\n      <td className=\"flex items-baseline py-2 pr-3 text-base dark:text-slate-400\">\n        <div className=\"flex-1 ml-1\">{title}</div>\n      </td>\n      <td\n        onMouseOver={() => setHovering(true)}\n        onMouseOut={() => setHovering(false)}\n        className={`relative w-full h-full pl-2 py-2 text-base text-slate-800 transition dark:text-slate-300 break-all ${\n          hovering ? \"bg-slate-100 dark:bg-slate-700\" : \"bg-transparent\"\n        }`}\n      >\n        {value}\n        <div\n          className={`absolute top-0 right-0 flex justify-end h-full w-full transition ${\n            hovering ? \"opacity-100\" : \"opacity-0\"\n          }`}\n        >\n          <CopyTextButton\n            className=\"bg-slate-200 hover:bg-slate-300 h-fit mt-1 mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-600 dark:hover:bg-slate-500\"\n            value={value}\n          ></CopyTextButton>\n        </div>\n      </td>\n    </tr>\n  );\n};\n\nexport const DataTable: FunctionComponent<DataTableProps> = ({ rows }) => {\n  return (\n    <div>\n      <Title className=\"text-slate-700 dark:text-slate-400 mb-2\">\n        Properties\n      </Title>\n      <table className=\"w-full table-auto border-y-[0.5px] border-slate-300 transition dark:border-slate-700\">\n        <tbody className=\"divide-solid divide-y divide-slate-300 w-full transition dark:divide-slate-700\">\n          {rows.map((row) => {\n            return (\n              <DataRow\n                key={row.key}\n                title={row.key}\n                value={row.value}\n                icon={row.icon}\n              />\n            );\n          })}\n        </tbody>\n      </table>\n    </div>\n  );\n};\n"
  },
  {
    "path": "app/components/DocumentTitle.tsx",
    "content": "import { PencilAltIcon } from \"@heroicons/react/outline\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useFetcher } from \"remix\";\nimport { match } from \"ts-pattern\";\nimport { useJsonDoc } from \"~/hooks/useJsonDoc\";\n\nexport function DocumentTitle() {\n  const { doc } = useJsonDoc();\n  const [editedTitle, setEditedTitle] = useState(doc.title);\n  const updateDoc = useFetcher();\n  const ref = useRef<HTMLInputElement | null>(null);\n\n  useEffect(() => {\n    if (updateDoc.type === \"done\" && updateDoc.data.title) {\n      ref.current?.blur();\n    }\n  }, [updateDoc]);\n\n  if (doc.readOnly) {\n    return (\n      <div\n        className=\"flex justify-center items-center w-full\"\n        title={doc.title}\n      >\n        <span\n          className={\n            \"min-w-[15vw] border-none text-ellipsis text-slate-300 px-2 pl-10 py-1 rounded-sm bg-transparent placeholder:text-slate-400 focus:bg-black/30 focus:outline-none focus:border-none hover:cursor-text transition dark:bg-transparent dark:text-slate-200 dark:placeholder:text-slate-400 dark:focus:bg-black dark:focus:bg-opacity-10\"\n          }\n        >\n          {doc.title}\n        </span>\n      </div>\n    );\n  } else {\n    return (\n      <updateDoc.Form method=\"post\" action={`/actions/${doc.id}/update`}>\n        <div\n          className=\"flex justify-center items-center w-full\"\n          title={doc.title}\n        >\n          <label className=\"relative block group\">\n            <PencilAltIcon className=\"h-5 w-5 absolute top-1/2 transform -translate-y-1/2 left-3 text-white opacity-0 transition pointer-events-none group-hover:opacity-80 group-focus:opacity-80\" />\n            <input\n              ref={ref}\n              className={\n                \"min-w-[15vw] border-none text-ellipsis text-slate-300 px-2 pl-10 py-1 rounded-sm bg-transparent placeholder:text-slate-400 focus:bg-black/30 focus:outline-none focus:border-none hover:bg-black hover:bg-opacity-30 hover:cursor-text transition dark:bg-transparent dark:text-slate-200 dark:placeholder:text-slate-400 dark:focus:bg-black dark:focus:bg-opacity-10 dark:hover:bg-black dark:hover:bg-opacity-10\"\n              }\n              type=\"text\"\n              name=\"title\"\n              spellCheck=\"false\"\n              placeholder=\"Name your JSON file\"\n              value={editedTitle}\n              onChange={(e) => setEditedTitle(e.target.value)}\n            />\n          </label>\n\n          {match(editedTitle)\n            .with(doc.title, () => (\n              <p className=\"ml-2 text-transparent\">Save</p>\n            ))\n            .with(\"\", () => (\n              <button\n                className=\"ml-2 text-lime-500 hover:text-lime-600 transition\"\n                onClick={() => setEditedTitle(doc.title)}\n              >\n                Reset\n              </button>\n            ))\n            .otherwise(() => (\n              <button\n                type=\"submit\"\n                className=\"ml-2 text-lime-500 hover:text-lime-600 transition\"\n              >\n                Save\n              </button>\n            ))}\n        </div>\n      </updateDoc.Form>\n    );\n  }\n}\n"
  },
  {
    "path": "app/components/DragAndDropForm.tsx",
    "content": "import { ArrowCircleDownIcon } from \"@heroicons/react/outline\";\nimport { useCallback, useRef } from \"react\";\nimport { useDropzone } from \"react-dropzone\";\nimport { Form, useSubmit } from \"remix\";\nimport invariant from \"tiny-invariant\";\n\nexport function DragAndDropForm() {\n  const formRef = useRef<HTMLFormElement>(null);\n  const filenameInputRef = useRef<HTMLInputElement>(null);\n  const rawJsonInputRef = useRef<HTMLInputElement>(null);\n\n  const submit = useSubmit();\n\n  const onDrop = useCallback(\n    (acceptedFiles: Array<File>) => {\n      if (!formRef.current || !filenameInputRef.current) {\n        return;\n      }\n\n      if (acceptedFiles.length === 0) {\n        return;\n      }\n\n      const firstFile = acceptedFiles[0];\n\n      const reader = new FileReader();\n\n      reader.onabort = () => console.log(\"file reading was aborted\");\n      reader.onerror = () => console.log(\"file reading has failed\");\n      reader.onload = () => {\n        if (reader.result == null) {\n          return;\n        }\n\n        let jsonValue: string | undefined = undefined;\n\n        if (typeof reader.result === \"string\") {\n          jsonValue = reader.result;\n        } else {\n          const decoder = new TextDecoder(\"utf-8\");\n          jsonValue = decoder.decode(reader.result);\n        }\n\n        invariant(rawJsonInputRef.current, \"rawJsonInputRef is null\");\n        invariant(jsonValue, \"jsonValue is undefined\");\n\n        rawJsonInputRef.current.value = jsonValue;\n\n        submit(formRef.current);\n      };\n      reader.readAsArrayBuffer(firstFile);\n      filenameInputRef.current.value = firstFile.name;\n    },\n    [formRef.current, filenameInputRef.current, rawJsonInputRef.current]\n  );\n\n  const { getRootProps, getInputProps, isDragActive } = useDropzone({\n    onDropAccepted: onDrop,\n    maxFiles: 1,\n    maxSize: 1024 * 1024 * 1,\n    multiple: false,\n    accept: \"application/json\",\n  });\n\n  return (\n    <Form method=\"post\" action=\"/actions/createFromFile\" ref={formRef}>\n      <div\n        {...getRootProps()}\n        className=\"block min-w-[300px] cursor-pointer rounded-md border-2 border-dashed border-slate-600 bg-slate-900/40 p-4 text-base text-slate-300 focus:border-indigo-500 focus:ring-indigo-500\"\n      >\n        <input {...getInputProps()} />\n        <div className=\"flex items-center\">\n          <ArrowCircleDownIcon\n            className={`mr-3 inline h-6 w-6 ${\n              isDragActive ? \"text-lime-500\" : \"\"\n            }`}\n          />\n          <p className={`${isDragActive ? \"text-lime-500\" : \"\"}`}>\n            {isDragActive\n              ? \"Now drop to open it…\"\n              : \"Drop a JSON file here, or click to select\"}\n          </p>\n        </div>\n\n        <input type=\"hidden\" name=\"filename\" ref={filenameInputRef} />\n        <input type=\"hidden\" name=\"rawJson\" ref={rawJsonInputRef} />\n      </div>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "app/components/ExampleDoc.tsx",
    "content": "import { Link } from \"remix\";\n\nexport function ExampleDoc({\n  id,\n  title,\n  path,\n}: {\n  id: string;\n  title: string;\n  path?: string;\n}) {\n  return (\n    <Link\n      to={`/j/${id}${path ? `?path=${path}` : \"\"}`}\n      className=\"bg-slate-900 px-4 py-2 rounded-md whitespace-nowrap text-lime-300 transition hover:text-lime-500\"\n    >\n      {title}\n    </Link>\n  );\n}\n"
  },
  {
    "path": "app/components/ExampleUrl.tsx",
    "content": "import { Form } from \"remix\";\n\nexport function ExampleUrl({\n  url,\n  title,\n  displayTitle,\n}: {\n  url: string;\n  title: string;\n  displayTitle?: string;\n}) {\n  return (\n    <Form\n      method=\"post\"\n      action=\"/actions/createFromUrl?utm_source=example_url\"\n      reloadDocument\n    >\n      <input type=\"hidden\" name=\"jsonUrl\" value={url} />\n      <input type=\"hidden\" name=\"title\" value={title} />\n      <button\n        type=\"submit\"\n        className=\"bg-slate-900 px-4 py-2 rounded-md whitespace-nowrap text-lime-300 transition hover:text-lime-500\"\n      >\n        {displayTitle ?? title}\n      </button>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "app/components/FileSelector/FileDropzone.tsx",
    "content": "import { FunctionComponent, useCallback } from \"react\";\nimport { useDropzone } from \"react-dropzone\";\nimport { DocumentDownloadIcon } from \"@heroicons/react/outline\";\n\nexport const FileDropzone: FunctionComponent = ({ children }) => {\n  const onDrop = useCallback((acceptedFiles) => {\n    acceptedFiles.forEach((file: Blob) => {\n      const reader = new FileReader();\n      reader.onabort = () => console.log(\"file reading was aborted\");\n      reader.onerror = () => console.log(\"file reading has failed\");\n      reader.onload = () => {\n        if (typeof reader.result === \"string\") {\n          let json = JSON.parse(reader.result);\n          // dataSourceDispatch(setJSONAction(\"Needs title\", json));\n        } else {\n          // dataSourceDispatch(setErrorAction(\"Can't read file\"));\n        }\n      };\n      reader.readAsText(file);\n    });\n  }, []);\n\n  const { getRootProps, isDragActive } = useDropzone({\n    onDrop,\n    multiple: false,\n    maxFiles: 1,\n    accept: \"application/json, text/*\",\n    noDragEventsBubbling: true,\n  });\n\n  return (\n    <div\n      {...getRootProps()}\n      className={\"absolute w-screen h-screen m-0 p-0 left-0 top-0\"}\n    >\n      <div\n        className={`${\n          isDragActive ? \"\" : \"hidden\"\n        } absolute w-screen h-screen bg-black bg-opacity-50 flex justify-center items-center`}\n      >\n        <div className={\"text-center\"}>\n          {/*<input {...getInputProps()} />*/}\n          <DocumentDownloadIcon className={\"w-72 h-72 text-white\"} />\n          <p className={\"text-white text-2xl\"}>\n            Drag 'n' drop some files here, or click to select files\n          </p>\n        </div>\n      </div>\n      <div>{children}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "app/components/Footer.tsx",
    "content": "import { useJsonDoc } from \"~/hooks/useJsonDoc\";\nimport { ArrowKeysIcon } from \"./Icons/ArrowKeysIcon\";\nimport { CopyShortcutIcon } from \"./Icons/CopyShortcutIcon\";\nimport { EscapeKeyIcon } from \"./Icons/EscapeKeyIcon\";\nimport { SquareBracketsIcon } from \"./Icons/SquareBracketsIcon\";\nimport { Body } from \"./Primitives/Body\";\nimport { ThemeModeToggler } from \"./ThemeModeToggle\";\nimport { GithubStarSmall } from \"./UI/GithubStarSmall\";\nimport { IndentPreference } from \"~/components/IndentPreference\";\nimport { ArrowRightIcon } from \"@heroicons/react/outline\";\nimport TriggerDevLogoImageDark from \"~/assets/images/trigger-dev-logo-dark.png\";\nimport TriggerDevLogoImage from \"~/assets/images/trigger-dev-logo.png\";\nimport TriggerDevLogoTriangleImage from \"~/assets/images/td-triangle.png\";\n\nexport function Footer() {\n  const { minimal } = useJsonDoc();\n\n  return (\n    <footer className=\"flex items-center justify-between w-screen h-[32px] flex-shrink-0 bg-slate-200 dark:bg-slate-800 border-t-[1px] border-slate-400 transition dark:border-slate-600\">\n      <ol className=\"flex pl-3\">\n        <li className=\"flex items-center\">\n          <ArrowKeysIcon className=\"transition text-slate-300 dark:text-slate-500\" />\n          <Body className=\"pl-2 pr-4 text-slate-800 transition dark:text-white\">\n            Navigate\n          </Body>\n        </li>\n        <li className=\"flex items-center\">\n          <SquareBracketsIcon className=\"transition text-slate-300 dark:text-slate-500\" />\n          <Body className=\"pl-2 pr-4 text-slate-800 transition dark:text-white\">\n            History\n          </Body>\n        </li>\n        <li className=\"flex items-center\">\n          <EscapeKeyIcon className=\"transition text-slate-300 dark:text-slate-500\" />\n          <Body className=\"pl-2 pr-4 text-slate-800 transition dark:text-white whitespace-nowrap\">\n            Reset path\n          </Body>\n        </li>\n        <li className=\"flex items-center\">\n          <CopyShortcutIcon className=\"transition text-slate-300 dark:text-slate-500\" />\n          <Body className=\"flex pl-2 pr-4 text-slate-800 transition dark:text-white\">\n            Copy&nbsp;\n            <span className=\"hidden lg:flex whitespace-nowrap\">\n              selected&nbsp;\n            </span>\n            node\n          </Body>\n        </li>\n      </ol>\n      <ol className=\"flex gap-2 items-center h-full invisible md:visible\">\n        {minimal && (\n          <li>\n            <GithubStarSmall />\n          </li>\n        )}\n        <li>\n          <IndentPreference />\n        </li>\n        <li>\n          <ThemeModeToggler />\n        </li>\n      </ol>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "app/components/Header.tsx",
    "content": "import { ShareIcon, PlusIcon, TrashIcon } from \"@heroicons/react/outline\";\nimport { DocumentTitle } from \"./DocumentTitle\";\nimport { DiscordIconTransparent } from \"./Icons/DiscordIconTransparent\";\nimport { EmailIconTransparent } from \"./Icons/EmailIconTransparent\";\nimport { GithubStar } from \"./UI/GithubStar\";\nimport { Logo } from \"./Icons/Logo\";\nimport { Share } from \"./Share\";\nimport { NewDocument } from \"./NewDocument\";\nimport {\n  Popover,\n  PopoverArrow,\n  PopoverContent,\n  PopoverTrigger,\n} from \"./UI/Popover\";\nimport { Form } from \"remix\";\nimport { useJsonDoc } from \"~/hooks/useJsonDoc\";\nimport { LogoTriggerdotdev } from \"./Icons/LogoTriggerdotdev\";\n\nexport function Header() {\n  const { doc } = useJsonDoc();\n\n  return (\n    <header className=\"flex items-center justify-between w-screen h-[40px] bg-indigo-700 dark:bg-slate-800 border-b-[1px] border-slate-600\">\n      <div className=\"flex pl-2 gap-1 sm:gap-1.5 pt-0.5 h-8 justify-center items-center\">\n        <div className=\"w-20 sm:w-24\">\n          <Logo />\n        </div>\n        <p className=\"text-slate-300 text-sm font-sans\">by</p>\n        <LogoTriggerdotdev className=\"w-16 sm:w-20 opacity-80 hover:opacity-100  transition duration-300\" />\n      </div>\n      <DocumentTitle />\n      <ol className=\"flex text-sm items-center gap-2 px-4\">\n        {!doc.readOnly && (\n          <Form\n            method=\"delete\"\n            onSubmit={(e) =>\n              !confirm(\n                \"This will permanantly delete this document from jsonhero.io, are you sure you want to continue?\"\n              ) && e.preventDefault()\n            }\n          >\n            <button type=\"submit\">\n              <button className=\"flex items-center justify-center py-1 bg-slate-200 text-slate-800 bg-opacity-80 text-base font-bold px-2 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition\">\n                <TrashIcon className=\"w-4 h-4 mr-0.5\"></TrashIcon>\n                Delete\n              </button>\n            </button>\n          </Form>\n        )}\n\n        <Popover>\n          <PopoverTrigger>\n            <button className=\"flex items-center justify-center bg-lime-500 text-slate-800 bg-opacity-90 text-base font-bold px-2 py-1 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition\">\n              <PlusIcon className=\"w-4 h-4 mr-0.5\"></PlusIcon>\n              New\n            </button>\n          </PopoverTrigger>\n          <PopoverContent side=\"bottom\" sideOffset={8}>\n            <NewDocument />\n            <PopoverArrow\n              className=\"fill-current text-indigo-700\"\n              offset={20}\n            />\n          </PopoverContent>\n        </Popover>\n\n        <Popover>\n          <PopoverTrigger>\n            <button className=\"flex items-center justify-center py-1 bg-slate-200 text-slate-800 bg-opacity-90 text-base font-bold px-2 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition\">\n              <ShareIcon className=\"w-4 h-4 mr-1\"></ShareIcon>\n              Share\n            </button>\n          </PopoverTrigger>\n          <PopoverContent side=\"bottom\" sideOffset={8}>\n            <Share />\n            <PopoverArrow\n              className=\"fill-current text-indigo-700\"\n              offset={20}\n            />\n          </PopoverContent>\n        </Popover>\n\n        <li className=\"opacity-90 transition hover:cursor-pointer hover:opacity-100\">\n          <GithubStar />\n        </li>\n        <li className=\"opacity-90 transition hover:cursor-pointer hover:opacity-100\">\n          <a href=\"https://discord.gg/JtBAxBr2m3\" target=\"_blank\">\n            <DiscordIconTransparent />\n          </a>\n        </li>\n      </ol>\n    </header>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeApiHeroBanner.tsx",
    "content": "import { Body } from \"../Primitives/Body\";\nimport { HomeApiHeroLaptop } from \"./HomeApiHeroLaptop\";\n\nexport function HomeApiHeroBanner() {\n  return (\n    <div className=\"flex items-center justify-start md:justify-center w-full h-40 bg-gradient-to-r from-purple-600 via-pink-500 to-purple-600 hover:backdrop-filter hover:backdrop-brightness-75 transition\">\n      <div className=\"relative flex justify-center items-center w-1/2 md:w-full pl-6 md:px-6\">\n        <div className=\"flex flex-col\">\n          <Body className=\" text-white text-[1rem] sm:text-[1.2rem] font-bold md:text-3xl leading-tight\">\n            Early access to ⚡️ API Hero\n          </Body>\n          <p className=\"mb-2 text-white md:text-xl text-sm\">\n            Make every API you use faster and more reliable.\n          </p>\n          <a\n            href=\"https://apihero.run\"\n            target=\"new\"\n            className=\"flex items-center justify-center px-3 py-2 mt-2 text-center text-md md:text-xl text-slate-800 font-bold bg-lime-500 rounded shadow-md hover:bg-lime-400 transition\"\n          >\n            Get started &rarr;\n          </a>\n        </div>\n        <a\n          href=\"https://apihero.run\"\n          target=\"new\"\n          className=\"absolute md:relative -top-5 md:top-auto -right-[20rem] md:right-auto\"\n        >\n          <HomeApiHeroLaptop className=\"w-50 md:w-80 mb-2\"></HomeApiHeroLaptop>\n        </a>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeApiHeroLaptop.tsx",
    "content": "import ApiHeroLaptop from \"~/assets/images/apihero-laptop.png\";\n\nexport type IconProps = {\n  className?: string;\n};\n\nexport function HomeApiHeroLaptop({ className }: IconProps) {\n  return <img src={ApiHeroLaptop} className={className} />;\n}\n"
  },
  {
    "path": "app/components/Home/HomeCollaborateSection.tsx",
    "content": "import { AutoplayVideo } from \"../AutoplayVideo\";\nimport { ExtraLargeTitle } from \"../Primitives/ExtraLargeTitle\";\nimport { SmallSubtitle } from \"../Primitives/SmallSubtitle\";\nimport { HomeSection } from \"./HomeSection\";\n\nimport shareVideo from \"~/assets/home/JsonHeroShare.mp4\";\n\nexport function HomeCollaborateSection() {\n  return (\n    <HomeSection\n      containerClassName=\"py-10 px-6 bg-black md:py-36 lg:py-20\"\n      reversed\n    >\n      <div className=\"w-full md:pl-10 md:w-1/2\">\n        <ExtraLargeTitle className=\"text-white mb-4\">\n          Collaborate with the whole world (and yourself)\n        </ExtraLargeTitle>\n        <SmallSubtitle className=\"mb-6 md:mb-10\">\n          Easily share your JSON documents with any distant relative. Link right\n          to the part of the document you're on. Or save the link for some\n          casual browsing later in the evening while enjoying a glass of red.\n        </SmallSubtitle>\n      </div>\n      <div className=\"w-full md:w-1/2\">\n        <AutoplayVideo src={shareVideo} />\n      </div>\n    </HomeSection>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeEdgeCasesSection.tsx",
    "content": "import { AutoplayVideo } from \"../AutoplayVideo\";\nimport { ExtraLargeTitle } from \"../Primitives/ExtraLargeTitle\";\nimport { SmallSubtitle } from \"../Primitives/SmallSubtitle\";\nimport { HomeSection } from \"./HomeSection\";\n\nimport edgeCasesVideo from \"~/assets/home/UncoverEdgeCases.mp4\";\n\nexport function HomeEdgeCasesSection() {\n  return (\n    <HomeSection\n      containerClassName=\"py-10 px-6 bg-black md:py-36 lg:py-20\"\n      reversed\n    >\n      <div className=\"w-full md:pl-10 md:w-1/2\">\n        <ExtraLargeTitle className=\"text-white mb-4\">\n          Uncover edge cases\n        </ExtraLargeTitle>\n        <SmallSubtitle className=\"mb-6 md:mb-10\">\n          Sometimes a field can be null, have an unexpected value or be missing\n          entirely. View any field's related values and see what to expect when\n          you least expect it. Or check out the inferred JSON schema to see what\n          your JSON is really made of.\n        </SmallSubtitle>\n      </div>\n      <div className=\"w-full md:w-1/2\">\n        <AutoplayVideo src={edgeCasesVideo} />\n      </div>\n    </HomeSection>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeFeatureGridSection.tsx",
    "content": "import {\n  FastForwardIcon,\n  MoonIcon,\n  ClockIcon,\n  CodeIcon,\n  LockOpenIcon,\n  CubeTransparentIcon,\n} from \"@heroicons/react/outline\";\nimport { Body } from \"../Primitives/Body\";\nimport { LargeTitle } from \"../Primitives/LargeTitle\";\nimport { HomeGridFeatureItem } from \"./HomeGridFeatureItem\";\nimport { HomeSection } from \"./HomeSection\";\n\nexport function HomeFeatureGridSection() {\n  return (\n    <HomeSection containerClassName=\"bg-black\">\n      <div className=\"flex flex-col px-4 pb-2 pt-6 md:py-12\">\n        <LargeTitle className=\"mb-4 text-slate-300\">\n          And lots more features…\n        </LargeTitle>\n        <div className=\"flex flex-col gap-4 md:flex-row md:flex-wrap\">\n          <HomeGridFeatureItem\n            icon={FastForwardIcon}\n            title=\"Keyboard shortcuts\"\n            titleClassName=\"text-white\"\n          >\n            <Body className=\"text-slate-400\">\n              Move as fast as you can think… after 3 coffees\n            </Body>\n          </HomeGridFeatureItem>\n\n          <HomeGridFeatureItem\n            icon={MoonIcon}\n            title=\"Dark mode\"\n            titleClassName=\"text-white\"\n          >\n            <Body className=\"text-slate-400\">\n              Of course, we’re not animals.\n            </Body>\n          </HomeGridFeatureItem>\n\n          <HomeGridFeatureItem\n            icon={ClockIcon}\n            title=\"Code view\"\n            titleClassName=\"text-white\"\n          >\n            <Body className=\"text-slate-400\">\n              Easily switch to the code view, so you can appear hardcore.\n            </Body>\n          </HomeGridFeatureItem>\n          <HomeGridFeatureItem\n            icon={CubeTransparentIcon}\n            title=\"Auto JSON Schema\"\n            titleClassName=\"text-white\"\n          >\n            <Body className=\"text-slate-400\">\n              Automatically generates JSON Schema (draft 2020-12) from your\n              JSON.\n            </Body>\n          </HomeGridFeatureItem>\n          <HomeGridFeatureItem\n            icon={CodeIcon}\n            title=\"VS Code plugin\"\n            titleClassName=\"text-white\"\n          >\n            <Body className=\"text-slate-400\">\n              Quickly view JSON files or selections in JSON Hero, right from VS\n              Code.{\" \"}\n              <a\n                className=\"whitespace-nowrap text-lime-300 hover:text-lime-500\"\n                href=\"https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                Get it here\n              </a>\n              .\n            </Body>\n          </HomeGridFeatureItem>\n          <HomeGridFeatureItem\n            icon={LockOpenIcon}\n            title=\"100% open source\"\n            titleClassName=\"text-white\"\n          >\n            <Body className=\"text-slate-400\">\n              Use jsonhero.io or fork it on GitHub and run it yourself.\n            </Body>\n          </HomeGridFeatureItem>\n        </div>\n      </div>\n    </HomeSection>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeFooter.tsx",
    "content": "import { Link } from \"remix\";\nimport { DiscordIcon } from \"../Icons/DiscordIcon\";\nimport { EmailIcon } from \"../Icons/EmailIcon\";\nimport { GithubIcon } from \"../Icons/GithubIcon\";\nimport { Logo } from \"../Icons/Logo\";\nimport { TwitterIcon } from \"../Icons/TwitterIcon\";\n\nexport type HomeFooterProps = {\n  maxWidth?: string;\n};\n\nexport function HomeFooter({ maxWidth = \"1150px\" }: HomeFooterProps) {\n  return (\n    <footer className=\"flex flex-col items-center w-full px-4 py-6 bg-black md:py-10\">\n      <div\n        className=\"flex items-center justify-between w-full border-t-[1px] pt-9 border-slate-800\"\n        style={{ maxWidth: maxWidth }}\n      >\n        <div className=\"flex flex-grow items-start\">\n          <Logo />\n        </div>\n        <ol className=\"flex ml-2\">\n          <li className=\"mr-2 hover:cursor-pointer text-white/70 hover:text-white transition\">\n            <Link to=\"/privacy\">Privacy</Link>\n          </li>\n          <li className=\"hover:cursor-pointer\">\n            <a\n              href=\"https://github.com/triggerdotdev/jsonhero-web\"\n              target=\"_blank\"\n            >\n              <GithubIcon />\n            </a>\n          </li>\n          <li className=\"ml-2 hover:cursor-pointer\">\n            <a href=\"mailto:hello@jsonhero.io\">\n              <EmailIcon />\n            </a>\n          </li>\n          <li className=\"ml-2 hover:cursor-pointer\">\n            <a href=\"https://discord.gg/JtBAxBr2m3\" target=\"_blank\">\n              <DiscordIcon />\n            </a>\n          </li>\n          <li className=\"ml-2 hover:cursor-pointer\">\n            <a href=\"https://twitter.com/triggerdotdev\" target=\"_blank\">\n              <TwitterIcon />\n            </a>\n          </li>\n        </ol>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeGithubBanner.tsx",
    "content": "import { Body } from \"../Primitives/Body\";\nimport { GithubStar } from \"../UI/GithubStar\";\n\nexport function GithubBanner() {\n  return (\n    <div className=\"flex items-center justify-center w-full h-14 bg-indigo-600\">\n      <div className=\"flex items-center\">\n        <Body className=\"mr-3 text-xl text-white\">Star us on GitHub 👉</Body>\n        <GithubStar />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeGridFeatureItem.tsx",
    "content": "import { IconComponent } from \"~/useColumnView\";\nimport { Body } from \"../Primitives/Body\";\nimport { Title } from \"../Primitives/Title\";\n\nexport type HomeGridFeatureItemProps = {\n  icon: IconComponent;\n  title: string;\n  className?: string;\n  titleClassName?: string;\n  children: React.ReactNode;\n};\n\nexport function HomeGridFeatureItem(props: HomeGridFeatureItemProps) {\n  return (\n    <div className=\"flex lg:basis-1/4 basis-1 md:basis-1/4 flex-grow flex-col p-6 rounded-sm bg-white bg-opacity-[7%]\">\n      <props.icon className=\"w-10 h-10 min-h-[44px] text-indigo-700 mb-3\" />\n      <Title className={props.titleClassName}>{props.title}</Title>\n      {props.children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeHeader.tsx",
    "content": "import { DiscordIconTransparent } from \"../Icons/DiscordIconTransparent\";\nimport { EmailIconTransparent } from \"../Icons/EmailIconTransparent\";\nimport { TwitterIcon } from \"../Icons/TwitterIcon\";\nimport { Logo } from \"../Icons/Logo\";\nimport { NewDocument } from \"../NewDocument\";\nimport { GithubStar } from \"../UI/GithubStar\";\nimport {\n  Popover,\n  PopoverArrow,\n  PopoverContent,\n  PopoverTrigger,\n} from \"../UI/Popover\";\nimport TriggerDevLogoImage from \"~/assets/images/trigger-dev-logo.png\";\nimport { LogoTriggerdotdev } from \"../Icons/LogoTriggerdotdev\";\n\nexport function HomeHeader({ fixed }: { fixed?: boolean }) {\n  return (\n    <header\n      className={`${\n        fixed ? \"fixed\" : \"\"\n      } z-20 flex h-12 justify-center  bg-indigo-700 flex-col`}\n    >\n      <div className=\"flex items-center justify-between w-screen px-4\">\n        <div className=\"flex gap-1 sm:gap-1.5 h-8 justify-center items-center\">\n          <div className=\"w-24 sm:w-32\">\n            <Logo />\n          </div>\n          <p className=\"text-slate-300 text-sm sm:text-base font-sans\">by</p>\n          <LogoTriggerdotdev className=\"pt-0.5 w-16 sm:w-24 opacity-80 hover:opacity-100 transition duration-300\" />\n        </div>\n        <ol className=\"flex items-center gap-2 sm:pr-4\">\n          <Popover>\n            <PopoverTrigger>\n              <button className=\" bg-lime-400 text-slate-900 text-lg font-bold px-2 py-0.5 rounded uppercase whitespace-nowrap cursor-pointer opacity-90 hover:opacity-100 transition\">\n                Try now\n              </button>\n            </PopoverTrigger>\n            <PopoverContent side=\"bottom\" sideOffset={30}>\n              <NewDocument />\n              <PopoverArrow\n                className=\"fill-current text-indigo-700\"\n                offset={20}\n              />\n            </PopoverContent>\n          </Popover>\n\n          <li className=\"hover:cursor-pointer hidden sm:block\">\n            <GithubStar />\n          </li>\n          <li className=\"hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block\">\n            <a href=\"mailto:hello@jsonhero.io\">\n              <EmailIconTransparent />\n            </a>\n          </li>\n          <li className=\"hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block\">\n            <a href=\"https://discord.gg/JtBAxBr2m3\" target=\"_blank\">\n              <DiscordIconTransparent />\n            </a>\n          </li>\n          <li className=\"hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block\">\n            <a href=\"https://twitter.com/triggerdotdev\" target=\"_blank\">\n              <TwitterIcon />\n            </a>\n          </li>\n        </ol>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeHeroSection.tsx",
    "content": "import { AutoplayVideo } from \"../AutoplayVideo\";\nimport { NewFile } from \"../NewFile\";\nimport { ExtraLargeTitle } from \"../Primitives/ExtraLargeTitle\";\nimport { SmallSubtitle } from \"../Primitives/SmallSubtitle\";\n\nimport heroVideo from \"~/assets/home/JsonHero2.mp4\";\n\nconst jsonHeroTitle = \"JSON sucks.\";\nconst jsonHeroSlogan = \"But we're making it better.\";\n\nexport function HomeHeroSection() {\n  return (\n    <div\n      className={`flex items-stretch flex-col md:flex-row bg-[rgb(56,52,139)] lg:p-6 lg:pb-16 pt-20 lg:pt-32`}\n    >\n      <div className=\"self-center md:w-1/2 md:pr-10 flex justify-end\">\n        <div className=\" max-w-3xl\">\n          <AutoplayVideo src={heroVideo} />\n        </div>\n      </div>\n      <div className=\"self-center flex align-center md:w-1/2 px-6 pb-8 mt-8 lg:mt-0\">\n        <div className=\"max-w-lg\">\n          <ExtraLargeTitle className=\"text-lime-300\">\n            {jsonHeroTitle}\n          </ExtraLargeTitle>\n          <ExtraLargeTitle className=\"text-white mb-4\">\n            {jsonHeroSlogan}\n          </ExtraLargeTitle>\n          <SmallSubtitle className=\"text-slate-200 mb-8\">\n            Stop staring at thousand line JSON files in your editor and start\n            staring at thousand line JSON files in the world's best JSON viewer.\n            With a few nice features to help make it not <em>the worst</em>.\n          </SmallSubtitle>\n          <NewFile />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeInfoBoxSection.tsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport { JsonProvider } from \"~/hooks/useJson\";\nimport { JsonColumnViewProvider, useJsonColumnViewAPI, } from \"~/hooks/useJsonColumnView\";\nimport { JsonDocProvider } from \"~/hooks/useJsonDoc\";\nimport { JsonPreview } from \"../JsonPreview\";\nimport { PreviewValue } from \"../Preview/PreviewValue\";\nimport { ExtraLargeTitle } from \"../Primitives/ExtraLargeTitle\";\nimport { SmallSubtitle } from \"../Primitives/SmallSubtitle\";\nimport { PropertiesValue } from \"../Properties/PropertiesValue\";\nimport { HomeSection } from \"./HomeSection\";\n\nconst json = {\n  id: \"a1c33bd1-0528-4de3-a745-44d95e7ac3d8\",\n  title: \"JSON Hero is a tool for JSON\",\n  thumbnail: \"https://media.giphy.com/media/13CoXDiaCcCoyk/giphy-downsized.gif\",\n  createdAt: \"2022-02-01T02:25:41-05:00\",\n  tint: \"#EAB308\",\n  webpages: \"https://www.theonion.com/\",\n  youtube: \"https://www.youtube.com/watch?v=dQw4w9WgXcQ\",\n  json: \"bourne\",\n};\n\nconst infoBoxData = [\n  {\n    title: \"Images\",\n    highlight: \"$.thumbnail\",\n  },\n  {\n    title: \"Dates\",\n    highlight: \"$.createdAt\",\n  },\n  {\n    title: \"Colors\",\n    highlight: \"$.tint\",\n  },\n  {\n    title: \"URLs\",\n    highlight: \"$.webpages\",\n  },\n  {\n    title: \"Videos\",\n    highlight: \"$.youtube\",\n  },\n];\n\nconst autoplayDuration = 3000;\n\nexport function HomeInfoBoxSection() {\n  return (\n    <SampleJSONPreview initialSelection={infoBoxData[0].highlight}>\n      <HomeInfoBoxSectionContent/>\n    </SampleJSONPreview>\n  );\n}\n\nfunction HomeInfoBoxSectionContent() {\n  const [index, setIndex] = useState(0);\n  const api = useJsonColumnViewAPI();\n  const interval = useRef<NodeJS.Timer | null>(null);\n\n  useEffect(() => {\n    const selectedPath = infoBoxData[index].highlight;\n    api.goToNodeId(selectedPath, \"home\");\n  }, [index]);\n\n  const resetInterval = () => {\n    if (interval.current != null) {\n      clearInterval(interval.current);\n    }\n    interval.current = setInterval(() => {\n      setIndex((i) => (i = (i + 1) % infoBoxData.length));\n    }, autoplayDuration);\n  };\n\n  useEffect(() => {\n    resetInterval();\n    return () => {\n      if (interval.current == null) return;\n      clearInterval(interval.current);\n    };\n  }, []);\n\n  return (\n    <HomeSection containerClassName=\"bg-black p-6\">\n      <div className=\"md:pr-4 lg:pr-10 flex flex-col w-full md:w-1/2\">\n        <ExtraLargeTitle className=\"text-white mb-4\">\n          <span className=\" text-lime-300\">{infoBoxData[index].title}</span> are\n          more than just strings\n        </ExtraLargeTitle>\n        <SmallSubtitle className=\"text-slate-400 mb-10\">\n          We figure out what your strings are made of, so you don't have to.\n        </SmallSubtitle>\n        <ul className=\"flex w-full text-slate-300 mb-3\">\n          {infoBoxData.map((value, i) => {\n            return (\n              <li\n                key={value.highlight}\n                onClick={() => {\n                  resetInterval();\n                  setIndex(i);\n                }}\n                className={`flex flex-grow justify-center px-4 py-2 cursor-pointer border-b-2 ${\n                  index === i\n                    ? \"text-white border-lime-500\"\n                    : \"border-slate-600\"\n                }`}\n              >\n                {value.title}\n              </li>\n            );\n          })}\n        </ul>\n        <div className=\"w-full\">\n          <JsonPreview\n            json={json}\n            highlightPath={infoBoxData[index].highlight}\n          />\n        </div>\n      </div>\n      <div className=\"relative w-full md:w-1/2 flex flex-col justify-center items-center py-5\">\n        <div className=\"pointer-events-none absolute z-10 bottom-0 w-full h-[200px] bg-gradient-to-t from-slate-900 to-transparent mb-5\"></div>\n        <div className=\"pointer-events-auto min-w-full max-w-full p-4 rounded-sm bg-slate-900 h-[65vh] overflow-y-auto custom-scrollbar\">\n          <div className=\"pointer-events-none\">\n            <div className=\"mb-4\">\n              <PreviewValue/>\n            </div>\n            <PropertiesValue/>\n          </div>\n        </div>\n      </div>\n    </HomeSection>\n  );\n}\n\nfunction SampleJSONPreview({\n  children,\n  initialSelection,\n}: {\n  children: React.ReactNode;\n  initialSelection: string;\n}) {\n  return (\n    <JsonDocProvider\n      doc={{\n        id: \"sample\",\n        title: \"Sample\",\n        type: \"raw\",\n        readOnly: false,\n        contents: \"\",\n      }}\n      path={initialSelection}\n    >\n      <JsonProvider initialJson={json}>\n        <JsonColumnViewProvider>{children}</JsonColumnViewProvider>\n      </JsonProvider>\n    </JsonDocProvider>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeSearchSection.tsx",
    "content": "import { AutoplayVideo } from \"../AutoplayVideo\";\nimport { ExtraLargeTitle } from \"../Primitives/ExtraLargeTitle\";\nimport { SmallSubtitle } from \"../Primitives/SmallSubtitle\";\nimport { HomeSection } from \"./HomeSection\";\n\nimport searchVideo from \"~/assets/home/JsonHeroSearch.mp4\";\n\nexport function HomeSearchSection() {\n  return (\n    <HomeSection containerClassName=\"py-10 px-6 bg-black md:py-36 lg:py-20\">\n      <div className=\"w-full md:pr-10 md:w-1/2\">\n        <ExtraLargeTitle className=\"text-white mb-4\">\n          Quickly search your whole JSON file\n        </ExtraLargeTitle>\n        <SmallSubtitle className=\"mb-6 md:mb-10\">\n          Search for absolutely anything in your JSON file with blistering\n          speed. Use the fuzzy matching and keyboard shortcuts to make\n          navigating your files even faster.\n        </SmallSubtitle>\n      </div>\n      <div className=\"w-full md:w-1/2\">\n        <AutoplayVideo src={searchVideo} />\n      </div>\n    </HomeSection>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeSection.tsx",
    "content": "export type HomeSectionProps = {\n  containerClassName?: string;\n  maxWidth?: string;\n  reversed?: boolean;\n  flipped?: boolean;\n  children: React.ReactNode;\n};\n\nexport function HomeSection({\n  containerClassName,\n  maxWidth = \"1150px\",\n  reversed = false,\n  flipped = false,\n  children,\n}: HomeSectionProps) {\n  return (\n    <div className={`flex justify-center items-center ${containerClassName}`}>\n      <div\n        className={`flex flex-col md:flex-row w-full ${\n          reversed ? \"md:flex-row-reverse\" : \"\"\n        }${flipped ? \"flex-col-reverse\" : \"\"}`}\n        style={{ maxWidth: maxWidth }}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Home/HomeSplitSection.tsx",
    "content": "import React from \"react\";\n\nexport type HomeSplitSectionProps = {\n  className?: string;\n  children: React.ReactNode;\n};\n\nexport function HomeSplitSection({\n  className,\n  children,\n}: HomeSplitSectionProps) {\n  return (\n    <div\n      className={`grid lg:grid-cols-2 items-center justify-items-center py-12 ${className}`}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function HomeSplitTextContent({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"justify-self-center lg:justify-self-end max-w-2xl px-20 flex flex-col justify-center\">\n      {children}\n    </div>\n  );\n}\n\nexport function HomeSplitMediaContent({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"flex justify-center items-center px-10 py-5\">\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/ArrayIcon.tsx",
    "content": "export function ArrayIcon(props: React.SVGProps<SVGSVGElement>) {\n\n  return (\n    <svg\n    className={props.className}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 24 24\"\n      key=\"array\"\n    >\n     <path d=\"M5.8899 18.525C5.8899 17.2 4.80855 17.025 3.84224 17.025C3.52013 17.025 3.22104 17.05 2.89893 17.05V2.95C4.00329 2.95 5.8899 3.25 5.8899 1.475C5.8899 0.525001 5.19968 0 4.37141 0H2.04766C0.80526 0 0 0.700001 0 2.1V17.925C0 19.35 0.782252 20 2.04766 20H4.37141C5.19968 20 5.8899 19.475 5.8899 18.525Z\" fill=\"currentColor\"/>\n     <path d=\"M20 17.925V2.1C20 0.700001 19.1947 0 17.9523 0H15.6286C14.8003 0 14.1101 0.525001 14.1101 1.475C14.1101 2.825 15.3065 2.975 16.2958 2.975C16.5489 2.975 16.825 2.95 17.1011 2.95V17.05C16.0197 17.05 14.1101 16.8 14.1101 18.525C14.1101 19.525 14.9154 20 15.7436 20H18.0904C19.3098 20 20 19.25 20 17.925Z\" fill=\"currentColor\"/>\n\n    </svg>\n  );\n};\n\n"
  },
  {
    "path": "app/components/Icons/ArrowKeysIcon.tsx",
    "content": "export function ArrowKeysIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"62\"\n      height=\"14\"\n      viewBox=\"0 0 62 14\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M6.60956 4.48804C6.80972 4.23784 7.19026 4.23784 7.39043 4.48804L10.3501 8.18765C10.612 8.51503 10.3789 9 9.95969 9H4.04031C3.62106 9 3.38797 8.51503 3.64988 8.18765L6.60956 4.48804Z\"\n        fill=\"black\"\n      />\n      <rect x=\"16\" width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M23.3904 9.51196C23.1903 9.76216 22.8097 9.76216 22.6096 9.51196L19.6499 5.81235C19.388 5.48496 19.6211 5 20.0403 5L25.9597 5C26.3789 5 26.612 5.48497 26.3501 5.81235L23.3904 9.51196Z\"\n        fill=\"black\"\n      />\n      <rect x=\"32\" width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M36.488 7.39044C36.2378 7.19028 36.2378 6.80974 36.488 6.60957L40.1877 3.64988C40.515 3.38797 41 3.62106 41 4.04031L41 9.95969C41 10.3789 40.515 10.612 40.1877 10.3501L36.488 7.39044Z\"\n        fill=\"black\"\n      />\n      <rect x=\"48\" width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M57.512 6.60956C57.7622 6.80972 57.7622 7.19026 57.512 7.39043L53.8123 10.3501C53.485 10.612 53 10.3789 53 9.95969L53 4.04031C53 3.62106 53.485 3.38797 53.8123 3.64988L57.512 6.60956Z\"\n        fill=\"black\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/ArrowKeysUpDownIcon.tsx",
    "content": "export function ArrowKeysUpDownIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"28\"\n      height=\"14\"\n      viewBox=\"0 0 30 14\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M6.60956 4.48804C6.80972 4.23784 7.19026 4.23784 7.39043 4.48804L10.3501 8.18765C10.612 8.51503 10.3789 9 9.95969 9H4.04031C3.62106 9 3.38797 8.51503 3.64988 8.18765L6.60956 4.48804Z\"\n        fill=\"black\"\n      />\n      <rect x=\"16\" width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M23.3904 9.51196C23.1903 9.76216 22.8097 9.76216 22.6096 9.51196L19.6499 5.81235C19.388 5.48496 19.6211 5 20.0403 5L25.9597 5C26.3789 5 26.612 5.48497 26.3501 5.81235L23.3904 9.51196Z\"\n        fill=\"black\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/CopyShortcutIcon.tsx",
    "content": "export function CopyShortcutIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"30\"\n      height=\"14\"\n      viewBox=\"0 0 30 14\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <rect x=\"16\" width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M5.64,10.22H8.25V7a.39.39,0,0,1,.38-.39H10l-3-3L4,6.65H5.26A.39.39,0,0,1,5.64,7v3.18Zm3,.78H5.26a.38.38,0,0,1-.39-.39V7.43H3.11a.39.39,0,0,1-.28-.66L6.72,2.86a.39.39,0,0,1,.55,0l3.88,3.91a.38.38,0,0,1-.27.66H9v3.18a.38.38,0,0,1-.39.39Z\"\n        stroke=\"#0f172a\" strokeWidth=\"0.35px\" fill=\"#0f172a\"\n      />\n      <path\n        d=\"M23.81,9.52a1.66,1.66,0,0,0,.53-.27,1.57,1.57,0,0,0,.35-.42,1.07,1.07,0,0,0,.13-.53h1.6a2.26,2.26,0,0,1-.25,1,2.87,2.87,0,0,1-.7.85,3.35,3.35,0,0,1-1,.58,3.56,3.56,0,0,1-1.22.21,3.73,3.73,0,0,1-1.55-.31,3.2,3.2,0,0,1-1.11-.84,3.61,3.61,0,0,1-.67-1.22,4.91,4.91,0,0,1-.23-1.5V6.88a4.91,4.91,0,0,1,.23-1.5,3.54,3.54,0,0,1,.67-1.22,3.33,3.33,0,0,1,1.11-.84,3.94,3.94,0,0,1,2.83-.09,3,3,0,0,1,1,.59,2.66,2.66,0,0,1,.67.92,2.94,2.94,0,0,1,.23,1.17h-1.6a1.48,1.48,0,0,0-.45-1.07,1.65,1.65,0,0,0-.52-.33,1.75,1.75,0,0,0-.65-.12,1.59,1.59,0,0,0-1.44.78,2.27,2.27,0,0,0-.31.8,4.53,4.53,0,0,0-.09.91v.24a4.63,4.63,0,0,0,.09.92,2.46,2.46,0,0,0,.3.8,1.67,1.67,0,0,0,.56.56,1.63,1.63,0,0,0,.89.22A2.05,2.05,0,0,0,23.81,9.52Z\"\n        fill=\"#0f172a\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/DiscordIcon.tsx",
    "content": "export function DiscordIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"#F8FAFC\" />\n      <path\n        d=\"M18.0881 7.3374C18.0116 7.27279 17.9402 7.2032 17.8637 7.14356C17.554 6.88097 17.2269 6.63856 16.8846 6.41792C16.4342 6.13677 15.9516 5.90824 15.4464 5.73702C15.0844 5.61277 14.7172 5.51835 14.35 5.40901C14.2837 5.40901 14.2786 5.38414 14.3092 5.3245C14.3398 5.26485 14.4061 5.14558 14.4469 5.05115C14.4538 5.03366 14.4667 5.0191 14.4835 5.01001C14.5003 5.00092 14.5198 4.99789 14.5387 5.00146C14.809 5.04619 15.0844 5.07601 15.3547 5.13069C15.8281 5.229 16.2896 5.3756 16.7316 5.56805C17.1998 5.76225 17.6502 5.99501 18.0779 6.26385C18.2267 6.353 18.3697 6.45094 18.5063 6.5571C18.5891 6.62989 18.6566 6.71764 18.7051 6.81552C19.1108 7.51363 19.4521 8.24546 19.7251 9.00236C20.1066 10.0234 20.3983 11.0742 20.5971 12.1435C20.7042 12.715 20.7909 13.2866 20.8674 13.8631C20.9184 14.216 20.9388 14.5788 20.9745 14.9366C20.9745 15.0559 20.9745 15.1702 21 15.2895C21 15.3164 20.9911 15.3425 20.9745 15.3641C20.462 15.9257 19.8549 16.398 19.1794 16.7606C18.5379 17.1017 17.8516 17.3558 17.1395 17.5161C16.7511 17.6096 16.3554 17.6711 15.9564 17.7H15.7116C15.701 17.7002 15.6904 17.6981 15.6807 17.6938C15.671 17.6895 15.6624 17.6831 15.6555 17.6752C15.4413 17.4068 15.2323 17.1334 15.0232 16.8551V16.8253C16.3606 16.3823 17.5548 15.6041 18.4859 14.5689C18.3788 14.6434 18.2819 14.718 18.1748 14.7826C17.8739 14.9665 17.5781 15.1504 17.267 15.3193C16.7354 15.61 16.1728 15.8433 15.5892 16.0151C14.6422 16.3069 13.6595 16.474 12.6671 16.5121H12.3713H11.8155C11.4011 16.5146 10.9871 16.4897 10.5762 16.4376C10.1887 16.3879 9.80109 16.3332 9.41351 16.2636C8.86661 16.1567 8.33068 16.002 7.81221 15.8014C7.15233 15.5479 6.523 15.2246 5.93553 14.8372L5.55306 14.5788C6.01711 15.0934 6.54864 15.5462 7.13396 15.9257C7.72153 16.3044 8.35541 16.6099 9.02084 16.8352L8.98514 16.8899L8.39358 17.6553C8.38145 17.6729 8.36453 17.6868 8.34472 17.6956C8.3249 17.7044 8.30298 17.7076 8.28138 17.705C7.93875 17.691 7.59775 17.6511 7.26145 17.5857C6.76756 17.4952 6.28289 17.3621 5.81314 17.1881C5.27458 16.9934 4.76114 16.7382 4.28323 16.4277C3.86783 16.1551 3.48621 15.8365 3.14601 15.4784C3.14601 15.4784 3.12051 15.4386 3.10011 15.4287C3.06012 15.3983 3.03012 15.3571 3.01381 15.3103C2.9975 15.2635 2.99559 15.2131 3.00831 15.1653L3.05421 14.6335C3.0899 14.2856 3.1205 13.9426 3.1664 13.5947C3.2123 13.2468 3.28879 12.7647 3.36529 12.3472C3.51174 11.5311 3.7093 10.7244 3.95685 9.93177C4.16738 9.2543 4.42116 8.59033 4.71671 7.94373C4.91624 7.50667 5.14275 7.08178 5.39497 6.6714C5.46939 6.5728 5.56514 6.49137 5.67544 6.43284C6.1388 6.11857 6.63239 5.84893 7.14925 5.62769C7.71444 5.38251 8.30641 5.20075 8.91375 5.08594L9.47981 5.00643C9.49599 5.00328 9.51279 5.00611 9.52694 5.01438C9.54108 5.02265 9.55155 5.03575 9.55631 5.05115L9.7042 5.33942C9.7297 5.38415 9.7042 5.39907 9.6685 5.40901C9.41351 5.47859 9.15854 5.54319 8.90865 5.61774C8.45618 5.75584 8.01886 5.93729 7.60313 6.15946C7.24627 6.34465 6.9052 6.5574 6.58319 6.79565C6.3588 6.9696 6.14462 7.14853 5.92533 7.32745C5.9235 7.33135 5.92255 7.33557 5.92255 7.33986C5.92255 7.34415 5.9235 7.3484 5.92533 7.35229L5.99163 7.32248C6.471 7.09882 6.95037 6.86522 7.43994 6.65647C8.00719 6.4106 8.59831 6.22081 9.20443 6.08991C9.61682 5.99062 10.0361 5.92083 10.459 5.88114C10.8414 5.84635 11.2239 5.82649 11.6013 5.80661C11.79 5.80661 11.9787 5.80661 12.1673 5.80661C12.5141 5.80661 12.866 5.8414 13.2128 5.86625C13.8437 5.91322 14.4686 6.01806 15.0793 6.17936C15.6332 6.32264 16.1739 6.51049 16.6959 6.74099L17.9606 7.33243L18.0218 7.36224L18.0881 7.3374ZM9.35232 10.5679C9.08643 10.5761 8.82881 10.66 8.6113 10.8093C8.39378 10.9586 8.2259 11.1667 8.12839 11.4079C7.98657 11.7022 7.93351 12.0296 7.97541 12.3522C8.01397 12.7406 8.19505 13.1024 8.48538 13.371C8.61754 13.5006 8.77761 13.6 8.95401 13.6619C9.13041 13.7238 9.31872 13.7467 9.50531 13.7289C9.68475 13.7178 9.85988 13.6705 10.0196 13.5901C10.1794 13.5097 10.3203 13.3979 10.4335 13.2617C10.7252 12.9245 10.8682 12.4886 10.8312 12.049C10.8196 11.7253 10.7096 11.4122 10.515 11.1494C10.3862 10.9659 10.2123 10.8166 10.0093 10.7151C9.80628 10.6135 9.58046 10.563 9.35232 10.5679ZM16.1094 12.1733C16.1148 11.8593 16.0319 11.55 15.8697 11.2787C15.7548 11.0583 15.5775 10.8747 15.3587 10.7496C15.14 10.6245 14.889 10.5632 14.6356 10.5729C14.451 10.578 14.2698 10.6219 14.1043 10.7017C13.9388 10.7815 13.793 10.8953 13.6769 11.0351C13.5285 11.203 13.4159 11.398 13.3459 11.6088C13.2758 11.8196 13.2496 12.0419 13.2689 12.2627C13.2861 12.6947 13.4787 13.1023 13.8043 13.3959C13.9417 13.5243 14.1072 13.6205 14.2883 13.6773C14.4694 13.7342 14.6614 13.7501 14.8498 13.7239C15.1962 13.6764 15.5095 13.4978 15.7218 13.2269C15.9694 12.9284 16.106 12.5571 16.1094 12.1733Z\"\n        fill=\"#4338CA\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/DiscordIconTransparent.tsx",
    "content": "export function DiscordIconTransparent(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24ZM17.9988 7.2591C18.0282 7.28536 18.0577 7.31177 18.0881 7.3374L18.0218 7.36224L17.9606 7.33243L16.6959 6.74099C16.1739 6.51049 15.6332 6.32264 15.0793 6.17936C14.4686 6.01806 13.8437 5.91322 13.2128 5.86625C13.1375 5.86085 13.0619 5.85498 12.9862 5.84911C12.7134 5.82793 12.4388 5.80661 12.1673 5.80661H11.6013L11.5799 5.80773C11.2095 5.82724 10.8342 5.84701 10.459 5.88114C10.0361 5.92083 9.61682 5.99062 9.20443 6.08991C8.59831 6.22081 8.00719 6.4106 7.43994 6.65647C7.07334 6.81278 6.71245 6.98304 6.35301 7.15262C6.23244 7.2095 6.11204 7.2663 5.99163 7.32248L5.92533 7.35229C5.9235 7.3484 5.92255 7.34415 5.92255 7.33986C5.92255 7.33557 5.9235 7.33135 5.92533 7.32745C5.97774 7.28469 6.02985 7.24194 6.08188 7.19925C6.24757 7.0633 6.41242 6.92803 6.58319 6.79565C6.9052 6.5574 7.24627 6.34465 7.60313 6.15946C8.01886 5.93729 8.45618 5.75584 8.90865 5.61774C9.08216 5.56598 9.25813 5.51901 9.43486 5.47184C9.51264 5.45108 9.59057 5.43027 9.6685 5.40901C9.7042 5.39907 9.7297 5.38415 9.7042 5.33942L9.5563 5.05115C9.55155 5.03575 9.54108 5.02265 9.52694 5.01438C9.51279 5.00611 9.49599 5.00328 9.47981 5.00643L8.91375 5.08594C8.30641 5.20075 7.71444 5.38251 7.14925 5.62769C6.63239 5.84893 6.1388 6.11857 5.67544 6.43284C5.56514 6.49137 5.46939 6.5728 5.39497 6.6714C5.14275 7.08178 4.91624 7.50667 4.71671 7.94374C4.42116 8.59033 4.16738 9.2543 3.95685 9.93176C3.7093 10.7244 3.51174 11.5311 3.36529 12.3472C3.28879 12.7647 3.2123 13.2468 3.1664 13.5947C3.13139 13.8601 3.10529 14.1225 3.07903 14.3865C3.07086 14.4686 3.06268 14.5509 3.05421 14.6335L3.00831 15.1653C2.99559 15.2131 2.9975 15.2635 3.01381 15.3103C3.03012 15.3571 3.06012 15.3983 3.10011 15.4287C3.12051 15.4386 3.14601 15.4784 3.14601 15.4784C3.48621 15.8365 3.86783 16.1551 4.28323 16.4277C4.76114 16.7382 5.27458 16.9934 5.81314 17.1881C6.28289 17.3621 6.76756 17.4952 7.26145 17.5857C7.59775 17.6511 7.93875 17.691 8.28138 17.705C8.30297 17.7076 8.3249 17.7044 8.34472 17.6956C8.36453 17.6868 8.38145 17.6729 8.39358 17.6553L8.98514 16.8899L9.02084 16.8352C8.35542 16.6099 7.72153 16.3044 7.13396 15.9257C6.54864 15.5462 6.01711 15.0934 5.55306 14.5788L5.93553 14.8372C6.523 15.2246 7.15233 15.5479 7.81221 15.8014C8.33068 16.002 8.86661 16.1567 9.41352 16.2636C9.80109 16.3332 10.1887 16.3879 10.5762 16.4376C10.9871 16.4897 11.4011 16.5146 11.8155 16.5121H12.3713H12.6671C13.6595 16.474 14.6422 16.3069 15.5892 16.0151C16.1728 15.8433 16.7354 15.61 17.267 15.3193C17.5426 15.1696 17.8062 15.0082 18.0719 14.8455C18.1061 14.8245 18.1404 14.8036 18.1748 14.7826C18.25 14.7372 18.3202 14.6869 18.3924 14.6351C18.423 14.6132 18.454 14.591 18.4859 14.5689C17.5548 15.6041 16.3606 16.3823 15.0232 16.8253V16.8551C15.2323 17.1334 15.4413 17.4068 15.6555 17.6751C15.6624 17.6831 15.671 17.6895 15.6807 17.6938C15.6904 17.6981 15.701 17.7002 15.7116 17.7H15.9564C16.3554 17.6711 16.7511 17.6096 17.1395 17.5161C17.8516 17.3558 18.5379 17.1017 19.1794 16.7606C19.8549 16.398 20.462 15.9257 20.9745 15.3641C20.9911 15.3425 21 15.3164 21 15.2895C20.9745 15.1702 20.9745 15.0559 20.9745 14.9366C20.9626 14.8173 20.9524 14.6975 20.9422 14.5776C20.9218 14.338 20.9014 14.0983 20.8674 13.8631C20.7909 13.2866 20.7042 12.715 20.5971 12.1435C20.3983 11.0742 20.1066 10.0234 19.7251 9.00236C19.4521 8.24545 19.1108 7.51363 18.7051 6.81552C18.6566 6.71764 18.5891 6.62989 18.5063 6.5571C18.3697 6.45094 18.2267 6.353 18.0779 6.26385C17.6502 5.99501 17.1998 5.76225 16.7316 5.56805C16.2896 5.3756 15.8281 5.229 15.3547 5.13069C15.1869 5.09676 15.0172 5.0724 14.848 5.04811C14.7445 5.03326 14.6412 5.01843 14.5387 5.00146C14.5198 4.99789 14.5003 5.00092 14.4835 5.01001C14.4667 5.0191 14.4538 5.03366 14.4469 5.05115C14.4163 5.12207 14.3712 5.207 14.3378 5.27016C14.3267 5.2911 14.3168 5.30965 14.3092 5.3245C14.2786 5.38414 14.2837 5.40901 14.35 5.40901C14.4686 5.44432 14.5872 5.47808 14.7056 5.51178C14.9538 5.58245 15.2013 5.6529 15.4464 5.73702C15.9516 5.90824 16.4342 6.13677 16.8846 6.41792C17.2269 6.63856 17.554 6.88097 17.8637 7.14356C17.9098 7.17954 17.9541 7.21915 17.9988 7.2591ZM8.6113 10.8093C8.82881 10.66 9.08643 10.5761 9.35232 10.5679C9.58046 10.563 9.80628 10.6135 10.0093 10.7151C10.2123 10.8166 10.3862 10.9659 10.515 11.1494C10.7096 11.4122 10.8196 11.7253 10.8312 12.049C10.8682 12.4886 10.7252 12.9245 10.4335 13.2617C10.3203 13.3979 10.1794 13.5097 10.0196 13.5901C9.85988 13.6705 9.68475 13.7178 9.50531 13.7289C9.31872 13.7467 9.13041 13.7238 8.95401 13.6619C8.77761 13.6 8.61754 13.5006 8.48538 13.371C8.19505 13.1024 8.01397 12.7406 7.97541 12.3522C7.93351 12.0296 7.98657 11.7022 8.12839 11.4079C8.2259 11.1667 8.39378 10.9586 8.6113 10.8093ZM15.8697 11.2787C16.0319 11.55 16.1148 11.8593 16.1094 12.1733C16.106 12.5571 15.9694 12.9284 15.7218 13.2269C15.5095 13.4978 15.1962 13.6764 14.8498 13.7239C14.6614 13.7501 14.4694 13.7342 14.2883 13.6773C14.1072 13.6205 13.9417 13.5243 13.8043 13.3959C13.4787 13.1023 13.2861 12.6947 13.2689 12.2627C13.2496 12.0419 13.2758 11.8196 13.3459 11.6088C13.4159 11.398 13.5285 11.203 13.6769 11.0351C13.793 10.8953 13.9388 10.7815 14.1043 10.7017C14.2698 10.6219 14.451 10.578 14.6356 10.5729C14.889 10.5632 15.14 10.6245 15.3587 10.7496C15.5775 10.8747 15.7548 11.0583 15.8697 11.2787Z\"\n        fill=\"#F8FAFC\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/EmailIcon.tsx",
    "content": "export function EmailIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    \n     <svg width=\"24\" \n     height=\"24\" \n     viewBox=\"0 0 24 24\" \n     fill=\"none\" \n     xmlns=\"http://www.w3.org/2000/svg\">\n<circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#4338CA\"/>\n<path d=\"M12 0C5.37251 0 0 5.37251 0 12C0 18.6275 5.37251 24 12 24C18.6275 24 24 18.6275 24 12C24 5.37251 18.6275 0 12 0ZM4.02864 7.38421C4.29607 6.6387 4.79768 6.14156 5.58914 5.98842C5.87824 5.9324 6.17928 5.91671 6.47473 5.91596C8.32732 5.90961 10.1799 5.91297 12.0329 5.91297C13.9553 5.91297 15.8781 5.90588 17.8005 5.91596C18.8852 5.92156 19.6614 6.44821 19.9732 7.33155C20.1017 7.69534 20.0629 7.93327 19.6685 8.12674C17.3475 9.26668 15.0508 10.4567 12.7134 11.5611C12.3287 11.743 11.709 11.7397 11.3232 11.5574C8.97087 10.4451 6.65774 9.24988 4.32296 8.09948C3.97634 7.92878 3.9136 7.70468 4.02864 7.38384V7.38421ZM20.0935 15.7821C20.089 17.1977 19.2277 18.0788 17.808 18.0825C13.9467 18.0926 10.085 18.0923 6.22373 18.0825C4.83466 18.0792 3.94572 17.2534 3.92032 15.9031C3.88708 14.1223 3.90986 12.3399 3.91397 10.5586C3.91397 10.4279 3.95879 10.2972 3.99502 10.0966C4.23406 10.2019 4.42268 10.2759 4.60346 10.3659C6.91658 11.52 9.23195 12.6708 11.5376 13.8395C11.8685 14.0072 12.1311 14.0083 12.4624 13.8403C14.7677 12.6716 17.0827 11.5212 19.3954 10.367C19.5777 10.2763 19.7671 10.1993 20.058 10.069C20.0741 10.3752 20.0924 10.5635 20.0928 10.7514C20.095 12.428 20.098 14.1051 20.0928 15.7817L20.0935 15.7821Z\" fill=\"white\"/>\n</svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/EmailIconTransparent.tsx",
    "content": "export function EmailIconTransparent(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    \n      <svg width=\"24\" \n      height=\"24\" \n      viewBox=\"0 0 24 24\" \n      fill=\"none\" \n      xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 0C5.37251 0 0 5.37251 0 12C0 18.6275 5.37251 24 12 24C18.6275 24 24 18.6275 24 12C24 5.37251 18.6275 0 12 0ZM4.02864 7.38421C4.29607 6.6387 4.79768 6.14156 5.58914 5.98842C5.87824 5.9324 6.17928 5.91671 6.47473 5.91596C8.32732 5.90961 10.1799 5.91297 12.0329 5.91297C13.9553 5.91297 15.8781 5.90588 17.8005 5.91596C18.8852 5.92156 19.6614 6.44821 19.9732 7.33155C20.1017 7.69534 20.0629 7.93327 19.6685 8.12674C17.3475 9.26668 15.0508 10.4567 12.7134 11.5611C12.3287 11.743 11.709 11.7397 11.3232 11.5574C8.97087 10.4451 6.65774 9.24988 4.32296 8.09948C3.97634 7.92878 3.9136 7.70468 4.02864 7.38384V7.38421ZM20.0935 15.7821C20.089 17.1977 19.2277 18.0788 17.808 18.0825C13.9467 18.0926 10.085 18.0923 6.22373 18.0825C4.83466 18.0792 3.94572 17.2534 3.92032 15.9031C3.88708 14.1223 3.90986 12.3399 3.91397 10.5586C3.91397 10.4279 3.95879 10.2972 3.99502 10.0966C4.23406 10.2019 4.42268 10.2759 4.60346 10.3659C6.91658 11.52 9.23195 12.6708 11.5376 13.8395C11.8685 14.0072 12.1311 14.0083 12.4624 13.8403C14.7677 12.6716 17.0827 11.5212 19.3954 10.367C19.5777 10.2763 19.7671 10.1993 20.058 10.069C20.0741 10.3752 20.0924 10.5635 20.0928 10.7514C20.095 12.428 20.098 14.1051 20.0928 15.7817L20.0935 15.7821Z\" fill=\"white\"/>\n</svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/EscapeKeyIcon.tsx",
    "content": "export function EscapeKeyIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"14\"\n      height=\"14\"\n      viewBox=\"0 0 14 14\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M3.21695 10C2.79876 10 2.42068 9.88168 2.08269 9.64504C1.75044 9.4084 1.48693 9.0687 1.29216 8.62595C1.09739 8.17557 1 7.63359 1 7C1 6.38168 1.09739 5.85114 1.29216 5.4084C1.49265 4.95802 1.75044 4.61069 2.06551 4.36641C2.38058 4.12214 2.71283 4 3.06228 4C3.48619 4 3.83563 4.12595 4.1106 4.37786C4.3913 4.62214 4.59753 4.9542 4.72928 5.37405C4.86677 5.79389 4.93551 6.25954 4.93551 6.77099C4.93551 6.93893 4.92692 7.10305 4.90973 7.26336C4.89827 7.41603 4.88682 7.52672 4.87536 7.59542H2.42641C2.49515 7.93893 2.61831 8.17939 2.7959 8.31679C2.97348 8.44656 3.18257 8.51145 3.42317 8.51145C3.69814 8.51145 3.9903 8.39695 4.29964 8.16794L4.78084 9.33588C4.5517 9.54962 4.29391 9.71374 4.00749 9.82824C3.72106 9.94275 3.45754 10 3.21695 10ZM2.40922 6.31298H3.68096C3.68096 6.0916 3.638 5.90076 3.55207 5.74046C3.47187 5.57252 3.32006 5.48855 3.09665 5.48855C2.93625 5.48855 2.79303 5.55344 2.66701 5.68321C2.54098 5.81298 2.45505 6.0229 2.40922 6.31298Z\"\n        fill=\"#0F172A\"\n      />\n      <path\n        d=\"M7.06968 10C6.79471 10 6.50256 9.92748 6.19322 9.78244C5.8896 9.62977 5.62609 9.43511 5.40268 9.19847L6.05573 7.98473C6.44527 8.36641 6.79471 8.55725 7.10405 8.55725C7.25872 8.55725 7.36757 8.53053 7.43058 8.4771C7.49932 8.41603 7.53369 8.32824 7.53369 8.21374C7.53369 8.06107 7.45063 7.94275 7.2845 7.85878C7.11838 7.76718 6.92647 7.66412 6.70879 7.54962C6.54266 7.45801 6.37653 7.34351 6.2104 7.20611C6.05 7.06107 5.91538 6.87786 5.80654 6.65649C5.6977 6.43511 5.64327 6.16794 5.64327 5.85496C5.64327 5.29008 5.80081 4.83969 6.11588 4.50382C6.43095 4.16794 6.84054 4 7.34465 4C7.69982 4 8.00344 4.08015 8.25549 4.24046C8.51328 4.39313 8.73669 4.56489 8.92573 4.75573L8.27268 5.92366C8.11801 5.77099 7.9662 5.65267 7.81726 5.5687C7.66832 5.48473 7.52797 5.44275 7.39621 5.44275C7.14415 5.44275 7.01813 5.54962 7.01813 5.76336C7.01813 5.9084 7.09546 6.0229 7.25013 6.10687C7.41053 6.18321 7.59671 6.27481 7.80866 6.38168C7.98052 6.46565 8.14951 6.57634 8.31564 6.71374C8.4875 6.85115 8.62785 7.03054 8.73669 7.25191C8.85126 7.47328 8.90855 7.75573 8.90855 8.09924C8.90855 8.63359 8.75101 9.08397 8.43594 9.45038C8.1266 9.81679 7.67118 10 7.06968 10Z\"\n        fill=\"#0F172A\"\n      />\n      <path\n        d=\"M11.5736 10C11.1669 10 10.8002 9.88168 10.4737 9.64504C10.1472 9.4084 9.88654 9.0687 9.69177 8.62595C9.50273 8.17557 9.4082 7.63359 9.4082 7C9.4082 6.36641 9.51418 5.82825 9.72614 5.3855C9.94382 4.93512 10.2274 4.5916 10.5768 4.35496C10.9263 4.11832 11.3044 4 11.7111 4C11.9689 4 12.2009 4.05343 12.4071 4.16031C12.6191 4.26718 12.8052 4.41221 12.9656 4.59542L12.2782 5.85496C12.1865 5.74809 12.1035 5.67557 12.029 5.6374C11.9545 5.59924 11.8772 5.58015 11.797 5.58015C11.522 5.58015 11.3072 5.70992 11.1525 5.96947C10.9979 6.22137 10.9205 6.56489 10.9205 7C10.9205 7.43511 11.0007 7.78244 11.1611 8.04198C11.3215 8.29389 11.5163 8.41985 11.7455 8.41985C11.8657 8.41985 11.9832 8.3855 12.0978 8.31679C12.2181 8.24046 12.3298 8.15267 12.4329 8.05344L13 9.33588C12.788 9.58779 12.5532 9.76336 12.2954 9.8626C12.0376 9.9542 11.797 10 11.5736 10Z\"\n        fill=\"#0F172A\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/GithubIcon.tsx",
    "content": "export function GithubIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"#F8FAFC\" />\n      <path\n        d=\"M21 12.0523C20.9915 12.1541 20.982 12.2565 20.9741 12.3588C20.8837 13.6147 20.5087 14.8358 19.8759 15.935C19.1892 17.1392 18.2177 18.1681 17.0412 18.9371C16.376 19.3775 15.6509 19.7259 14.8867 19.9725C14.6521 20.0485 14.4508 19.9604 14.2906 19.7886C14.1508 19.642 14.0747 19.4487 14.0779 19.249C14.0779 18.3672 14.0779 17.4853 14.0779 16.6031C14.0863 16.3681 14.0628 16.1329 14.008 15.9038C13.9792 15.7993 13.9324 15.6997 13.8918 15.5924C14.0311 15.5634 14.1766 15.5377 14.3216 15.5032C15.1958 15.3068 16.0136 14.9915 16.7231 14.4432C17.5308 13.8177 18.0294 13.0111 18.176 12.0157C18.3187 11.0531 18.2037 10.1195 17.7728 9.233C17.6063 8.89501 17.3874 8.58384 17.1236 8.31036L17.0903 8.2737C17.3809 7.63017 17.4174 6.90541 17.193 6.23744C17.1338 6.05812 17.0463 5.88882 16.9335 5.73562C16.9232 5.71873 16.9081 5.70507 16.89 5.69624C16.8719 5.68742 16.8516 5.6838 16.8314 5.68583C16.1357 5.70242 15.4598 5.91436 14.8856 6.29599C14.6578 6.44107 14.4484 6.61171 14.2618 6.80437C14.253 6.8169 14.2399 6.82597 14.2248 6.82998C14.2097 6.83399 14.1936 6.83267 14.1795 6.82626C13.6814 6.65717 13.1643 6.58603 12.6409 6.54936C12.1713 6.51543 11.6997 6.51927 11.2308 6.56085C10.7502 6.59484 10.2762 6.68958 9.82082 6.84268C9.80583 6.84985 9.78867 6.85153 9.77251 6.84741C9.75634 6.84328 9.74225 6.83364 9.73283 6.82024C9.20278 6.26705 8.50548 5.89131 7.74132 5.74712C7.55238 5.711 7.35723 5.70771 7.16491 5.68801C7.14583 5.6851 7.1263 5.68795 7.10894 5.69617C7.09159 5.7044 7.07726 5.71759 7.0679 5.73398C6.87448 6.00312 6.75032 6.3133 6.70581 6.63856C6.63122 7.07823 6.66761 7.52889 6.81184 7.95192C6.85301 8.06958 6.91505 8.18012 6.97145 8.3027C6.90772 8.37329 6.83158 8.45374 6.75939 8.53746C6.25302 9.13009 5.92997 9.84975 5.82765 10.6131C5.72834 11.2252 5.75886 11.8505 5.91733 12.4507C6.1621 13.3378 6.6872 14.0377 7.44973 14.5713C8.04589 14.9899 8.71085 15.2624 9.41924 15.4408C9.65331 15.4999 9.88963 15.5503 10.1231 15.6012C10.0859 15.7046 10.0408 15.8086 10.0103 15.9175C9.94909 16.1717 9.92142 16.4324 9.92798 16.6934C9.93155 16.7171 9.92539 16.7411 9.91082 16.7604C9.89625 16.7796 9.87445 16.7925 9.85015 16.7963C9.51067 16.9011 9.15221 16.935 8.79827 16.8959C8.46398 16.8686 8.14004 16.7699 7.84961 16.607C7.58186 16.4549 7.35663 16.2415 7.19367 15.9853C6.92069 15.5618 6.54507 15.269 6.03915 15.1481C5.87828 15.104 5.70835 15.1016 5.54621 15.1409C5.51726 15.1481 5.48911 15.158 5.46217 15.1705C5.38152 15.2104 5.35557 15.2756 5.40577 15.3472C5.45449 15.4143 5.5113 15.4755 5.57497 15.5295C5.70582 15.6389 5.85359 15.7374 5.97429 15.8578C6.24219 16.126 6.4255 16.4505 6.5727 16.793C6.79548 17.3091 7.18352 17.6686 7.68661 17.9209C8.20945 18.1819 8.77007 18.2443 9.34762 18.1945C9.53769 18.1775 9.72663 18.1453 9.91896 18.1201C9.91896 18.1305 9.92234 18.1458 9.92234 18.1606C9.92234 18.5321 9.92572 18.9037 9.92234 19.2753C9.92243 19.3969 9.89197 19.5168 9.8336 19.6244C9.77522 19.7321 9.69069 19.8243 9.58732 19.8931C9.51652 19.946 9.4329 19.9803 9.34451 19.9927C9.25611 20.0052 9.1659 19.9954 9.08253 19.9643C6.95171 19.2321 5.31609 17.9187 4.17567 16.0242C3.63235 15.1126 3.27001 14.1103 3.10744 13.0691C2.81135 11.2274 3.13103 9.3421 4.01965 7.68951C4.90826 6.03693 6.31908 4.70396 8.04532 3.88597C8.87822 3.48473 9.7727 3.21731 10.6939 3.09412C10.9951 3.05418 11.2991 3.0394 11.602 3.00985C11.6251 3.00985 11.6471 3.00328 11.6702 3H12.3346C12.3572 3.00328 12.3792 3.00766 12.4023 3.00985C12.6685 3.03283 12.9353 3.04761 13.2003 3.0799C14.0888 3.18707 14.9544 3.42898 15.7655 3.79677C17.2635 4.46177 18.5425 5.5162 19.4608 6.84323C20.3464 8.10221 20.8692 9.56789 20.9752 11.0887C20.9831 11.1905 20.9914 11.2926 21 11.3951V12.0523Z\"\n        fill=\"#4338CA\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/GithubIconSimple.tsx",
    "content": "export type IconProps = {\n  className?: string;\n};\n\nexport function GithubIconSimple({ className }: IconProps) {\n  return (\n    <svg\n      className={className}\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_571_3822)\">\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M12 0C5.37017 0 0 5.50708 0 12.306C0 17.745 3.44015 22.3532 8.20626 23.9849C8.80295 24.0982 9.02394 23.7205 9.02394 23.3881C9.02394 23.0935 9.01657 22.3229 9.00921 21.2956C5.67219 22.0359 4.96501 19.6487 4.96501 19.6487C4.41989 18.2285 3.63168 17.8508 3.63168 17.8508C2.54144 17.0878 3.71271 17.1029 3.71271 17.1029C4.91344 17.1936 5.55433 18.372 5.55433 18.372C6.62247 20.2531 8.36096 19.7092 9.04604 19.3919C9.15654 18.5987 9.46593 18.0548 9.80479 17.745C7.13812 17.4353 4.33886 16.3777 4.33886 11.6638C4.33886 10.3192 4.80295 9.2238 5.57643 8.36261C5.4512 8.05288 5.03867 6.79887 5.69429 5.1067C5.69429 5.1067 6.7035 4.77432 8.99447 6.36827C9.95212 6.09632 10.9761 5.96034 12 5.95279C13.0166 5.95279 14.0479 6.09632 15.0055 6.36827C17.2965 4.77432 18.3057 5.1067 18.3057 5.1067C18.9613 6.79887 18.5488 8.05288 18.4236 8.36261C19.1897 9.2238 19.6538 10.3192 19.6538 11.6638C19.6538 16.3928 16.8471 17.4278 14.1731 17.7375C14.6004 18.1152 14.9908 18.8706 14.9908 20.0189C14.9908 21.6657 14.9761 22.9877 14.9761 23.3957C14.9761 23.728 15.1897 24.1058 15.8011 23.9849C20.5672 22.3532 24 17.745 24 12.3135C24 5.50708 18.6298 0 12 0Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_571_3822\">\n          <rect width=\"24\" height=\"24\" fill=\"white\" />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/LoadingIcon.tsx",
    "content": "export function LoadingIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"26\"\n      height=\"26\"\n      viewBox=\"0 0 26 26\"\n      fill=\"none\"\n    >\n      <circle\n        cx=\"13\"\n        cy=\"13\"\n        r=\"10\"\n        stroke=\"black\"\n        strokeOpacity=\"0.3\"\n        strokeWidth=\"4\"\n      />\n      <path\n        d=\"M13 23C7.47715 23 3 18.5228 3 13\"\n        stroke=\"#4338CA\"\n        strokeWidth=\"4\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/Logo.tsx",
    "content": "import { Link } from \"remix\";\n\nexport function Logo({\n  className,\n  width = \"100%\",\n}: {\n  className?: string;\n  width?: string;\n}) {\n  return (\n    <Link to=\"/\" aria-label=\"JSON Hero homepage\" className=\"w-40\">\n      <svg\n        className={className}\n        width={width}\n        height=\"50\"\n        viewBox=\"0 0 263 36\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"M94.8087 35.3033V1.39929H102.661L111.501 18.2473L114.829 25.7353H115.037C114.898 23.9326 114.707 21.922 114.465 19.7033C114.222 17.4846 114.101 15.37 114.101 13.3593V1.39929H121.381V35.3033H113.529L104.689 18.4033L101.361 11.0193H101.153C101.326 12.8913 101.517 14.902 101.725 17.0513C101.967 19.2006 102.089 21.2806 102.089 23.2913V35.3033H94.8087Z\"\n          fill=\"white\"\n        />\n        <path\n          d=\"M73.0419 35.9273C69.9912 35.9273 67.3045 35.2166 64.9819 33.7953C62.6939 32.3739 60.8912 30.3459 59.5739 27.7113C58.2912 25.0419 57.6499 21.8699 57.6499 18.1953C57.6499 14.4859 58.2912 11.3486 59.5739 8.78327C60.8912 6.18327 62.6939 4.20727 64.9819 2.85527C67.3045 1.4686 69.9912 0.775269 73.0419 0.775269C76.0925 0.775269 78.7619 1.4686 81.0499 2.85527C83.3725 4.20727 85.1752 6.18327 86.4579 8.78327C87.7752 11.3833 88.4339 14.5206 88.4339 18.1953C88.4339 21.8699 87.7752 25.0419 86.4579 27.7113C85.1752 30.3459 83.3725 32.3739 81.0499 33.7953C78.7619 35.2166 76.0925 35.9273 73.0419 35.9273ZM73.0419 29.3233C75.3645 29.3233 77.2019 28.3179 78.5539 26.3073C79.9059 24.2966 80.5819 21.5926 80.5819 18.1953C80.5819 14.7979 79.9059 12.1459 78.5539 10.2393C77.2019 8.3326 75.3645 7.37927 73.0419 7.37927C70.7192 7.37927 68.8819 8.3326 67.5299 10.2393C66.1779 12.1459 65.5019 14.7979 65.5019 18.1953C65.5019 21.5926 66.1779 24.2966 67.5299 26.3073C68.8819 28.3179 70.7192 29.3233 73.0419 29.3233Z\"\n          fill=\"white\"\n        />\n        <path\n          d=\"M40.7154 35.9273C38.4967 35.9273 36.278 35.5113 34.0593 34.6793C31.8753 33.8473 29.9167 32.6339 28.1833 31.0393L32.5513 25.7873C33.7647 26.8273 35.1167 27.6766 36.6073 28.3353C38.098 28.9939 39.5367 29.3233 40.9234 29.3233C42.518 29.3233 43.6967 29.0286 44.4594 28.4393C45.2567 27.8499 45.6553 27.0526 45.6553 26.0473C45.6553 24.9726 45.2047 24.1926 44.3034 23.7073C43.4367 23.1873 42.258 22.6153 40.7673 21.9913L36.3474 20.1193C35.2034 19.6339 34.1114 18.9926 33.0714 18.1953C32.0314 17.3633 31.182 16.3406 30.5233 15.1273C29.8647 13.9139 29.5354 12.4926 29.5354 10.8633C29.5354 8.99127 30.038 7.2926 31.0434 5.76727C32.0834 4.24193 33.5047 3.0286 35.3074 2.12727C37.1447 1.22594 39.242 0.775269 41.5993 0.775269C43.5407 0.775269 45.482 1.1566 47.4234 1.91927C49.3647 2.68194 51.0634 3.79127 52.5194 5.24727L48.6194 10.0833C47.51 9.2166 46.4007 8.55794 45.2914 8.10727C44.182 7.62194 42.9514 7.37927 41.5993 7.37927C40.282 7.37927 39.2247 7.6566 38.4273 8.21127C37.6647 8.73127 37.2834 9.4766 37.2834 10.4473C37.2834 11.4873 37.7687 12.2673 38.7393 12.7873C39.7447 13.3073 40.9754 13.8619 42.4314 14.4513L46.7994 16.2193C48.8447 17.0513 50.474 18.1953 51.6874 19.6513C52.9007 21.1073 53.5074 23.0313 53.5074 25.4233C53.5074 27.2953 53.0047 29.0286 51.9994 30.6233C50.994 32.2179 49.538 33.5006 47.6314 34.4713C45.7247 35.4419 43.4194 35.9273 40.7154 35.9273Z\"\n          fill=\"white\"\n        />\n        <path\n          d=\"M11.6583 35.9273C9.09298 35.9273 6.92631 35.4246 5.15831 34.4193C3.39031 33.3793 1.91698 31.8366 0.738312 29.7913L5.93831 25.9433C6.56231 27.0873 7.29031 27.9366 8.12231 28.4913C8.95431 29.046 9.80365 29.3233 10.6703 29.3233C12.057 29.3233 13.097 28.9073 13.7903 28.0753C14.5183 27.2086 14.8823 25.6486 14.8823 23.3953V1.39929H22.5263V24.0193C22.5263 26.2033 22.145 28.1966 21.3823 29.9993C20.6196 31.802 19.4236 33.2406 17.7943 34.3153C16.1996 35.39 14.1543 35.9273 11.6583 35.9273Z\"\n          fill=\"white\"\n        />\n        <path\n          d=\"M247.108 35.9273C244.058 35.9273 241.371 35.2166 239.048 33.7953C236.76 32.3739 234.958 30.3459 233.64 27.7113C232.358 25.0419 231.716 21.8699 231.716 18.1953C231.716 14.4859 232.358 11.3486 233.64 8.78327C234.958 6.18327 236.76 4.20727 239.048 2.85527C241.371 1.4686 244.058 0.775269 247.108 0.775269C250.159 0.775269 252.828 1.4686 255.116 2.85527C257.439 4.20727 259.242 6.18327 260.524 8.78327C261.842 11.3833 262.5 14.5206 262.5 18.1953C262.5 21.8699 261.842 25.0419 260.524 27.7113C259.242 30.3459 257.439 32.3739 255.116 33.7953C252.828 35.2166 250.159 35.9273 247.108 35.9273ZM247.108 29.3233C249.431 29.3233 251.268 28.3179 252.62 26.3073C253.972 24.2966 254.648 21.5926 254.648 18.1953C254.648 14.7979 253.972 12.1459 252.62 10.2393C251.268 8.3326 249.431 7.37927 247.108 7.37927C244.786 7.37927 242.948 8.3326 241.596 10.2393C240.244 12.1459 239.568 14.7979 239.568 18.1953C239.568 21.5926 240.244 24.2966 241.596 26.3073C242.948 28.3179 244.786 29.3233 247.108 29.3233Z\"\n          fill=\"#BFF164\"\n        />\n        <path\n          d=\"M201.438 35.3033V1.39929H213.658C216.05 1.39929 218.234 1.72863 220.21 2.38729C222.186 3.01129 223.763 4.08596 224.942 5.61129C226.12 7.13663 226.71 9.25129 226.71 11.9553C226.71 14.4513 226.155 16.514 225.046 18.1433C223.971 19.738 222.515 20.934 220.678 21.7313L228.374 35.3033H219.794L213.294 23.0833H209.082V35.3033H201.438ZM209.082 16.9993H213.034C215.044 16.9993 216.57 16.5833 217.61 15.7513C218.684 14.8846 219.222 13.6193 219.222 11.9553C219.222 10.2913 218.684 9.12996 217.61 8.47129C216.57 7.81263 215.044 7.48329 213.034 7.48329H209.082V16.9993Z\"\n          fill=\"#BFF164\"\n        />\n        <path\n          d=\"M172.949 35.3033V1.39929H194.165V7.84729H180.593V14.6593H192.137V21.0553H180.593V28.8553H194.685V35.3033H172.949Z\"\n          fill=\"#BFF164\"\n        />\n        <path\n          d=\"M137.91 35.3033V1.39929H145.554V14.4513H157.254V1.39929H164.95V35.3033H157.254V21.1593H145.554V35.3033H137.91Z\"\n          fill=\"#BFF164\"\n        />\n      </svg>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/LogoTriggerdotdev.tsx",
    "content": "export function LogoTriggerdotdev({\n  className,\n  width = \"100%\",\n}: {\n  className?: string;\n  width?: string;\n}) {\n  return (\n    <a href=\"https://trigger.dev/\" aria-label=\"Trigger.dev\">\n      <svg\n        className={`${className}`}\n        width={width}\n        height=\"30\"\n        viewBox=\"0 0 169 30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"M44.0084 4.04088H30.6671H31.1941V7.67807H35.686V23.329H39.489V7.67807H44.0084V4.04088Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M47.646 11.9215V9.55178H44.0911V23.329H47.646V16.7435C47.646 13.8503 49.9884 13.0236 51.8348 13.2441V9.27623C50.0986 9.27623 48.3625 10.0478 47.646 11.9215Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M55.6379 7.89851C56.8505 7.89851 57.8426 6.90655 57.8426 5.7217C57.8426 4.53686 56.8505 3.51733 55.6379 3.51733C54.453 3.51733 53.4609 4.53686 53.4609 5.7217C53.4609 6.90655 54.453 7.89851 55.6379 7.89851ZM53.8743 23.329H57.4292V9.55178H53.8743V23.329Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M70.9327 9.55179V11.2602C69.9681 9.96509 68.48 9.16603 66.5234 9.16603C62.6103 9.16603 59.6616 12.3623 59.6616 16.22C59.6616 20.1052 62.6103 23.2739 66.5234 23.2739C68.48 23.2739 69.9681 22.4749 70.9327 21.1798V22.6677C70.9327 24.8445 69.5548 26.0569 67.3226 26.0569C65.2007 26.0569 64.2913 25.2027 63.7126 24.1281L60.6812 25.864C61.8938 28.096 64.2637 29.2257 67.2124 29.2257C70.85 29.2257 74.4049 27.1867 74.4049 22.6677V9.55179H70.9327ZM67.0746 19.9949C64.8424 19.9949 63.2165 18.4243 63.2165 16.22C63.2165 14.0432 64.8424 12.4726 67.0746 12.4726C69.3068 12.4726 70.9327 14.0432 70.9327 16.22C70.9327 18.4243 69.3068 19.9949 67.0746 19.9949Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M87.8808 9.55179V11.2602C86.9163 9.96509 85.4282 9.16603 83.4716 9.16603C79.5584 9.16603 76.6097 12.3623 76.6097 16.22C76.6097 20.1052 79.5584 23.2739 83.4716 23.2739C85.4282 23.2739 86.9163 22.4749 87.8808 21.1798V22.6677C87.8808 24.8445 86.5029 26.0569 84.2708 26.0569C82.1488 26.0569 81.2394 25.2027 80.6607 24.1281L77.6294 25.864C78.8419 28.096 81.2119 29.2257 84.1605 29.2257C87.7981 29.2257 91.3531 27.1867 91.3531 22.6677V9.55179H87.8808ZM84.0227 19.9949C81.7906 19.9949 80.1647 18.4243 80.1647 16.22C80.1647 14.0432 81.7906 12.4726 84.0227 12.4726C86.2549 12.4726 87.8808 14.0432 87.8808 16.22C87.8808 18.4243 86.2549 19.9949 84.0227 19.9949Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M97.2782 17.9008H107.667C107.75 17.4324 107.805 16.964 107.805 16.4404C107.805 12.3899 104.912 9.16603 100.833 9.16603C96.5066 9.16603 93.5579 12.3348 93.5579 16.4404C93.5579 20.546 96.479 23.7148 101.109 23.7148C103.754 23.7148 105.821 22.6402 107.116 20.7665L104.25 19.1132C103.644 19.9123 102.542 20.4909 101.164 20.4909C99.2899 20.4909 97.7742 19.7194 97.2782 17.9008ZM97.2231 15.1454C97.6364 13.3819 98.9316 12.3623 100.833 12.3623C102.321 12.3623 103.809 13.1614 104.25 15.1454H97.2231Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M113.468 11.9215V9.55178H109.914V23.329H113.468V16.7435C113.468 13.8503 115.811 13.0236 117.657 13.2441V9.27623C115.921 9.27623 114.185 10.0478 113.468 11.9215Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M119.008 23.6874C120.303 23.6874 121.35 22.6403 121.35 21.3452C121.35 20.0502 120.303 19.0031 119.008 19.0031C117.712 19.0031 116.665 20.0502 116.665 21.3452C116.665 22.6403 117.712 23.6874 119.008 23.6874Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M133.944 4.04102V11.1776C132.952 9.91011 131.491 9.16616 129.479 9.16616C125.787 9.16616 122.755 12.3349 122.755 16.4405C122.755 20.5462 125.787 23.7149 129.479 23.7149C131.491 23.7149 132.952 22.9709 133.944 21.7034V23.3292H137.499V4.04102L133.944 4.04102ZM130.141 20.3257C127.936 20.3257 126.31 18.7551 126.31 16.4405C126.31 14.126 127.936 12.5553 130.141 12.5553C132.318 12.5553 133.944 14.126 133.944 16.4405C133.944 18.7551 132.318 20.3257 130.141 20.3257Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M143.203 17.9009H153.592C153.675 17.4325 153.73 16.9641 153.73 16.4406C153.73 12.39 150.837 9.16617 146.758 9.16617C142.432 9.16617 139.483 12.3349 139.483 16.4406C139.483 20.5462 142.404 23.7149 147.034 23.7149C149.679 23.7149 151.746 22.6403 153.041 20.7666L150.175 19.1133C149.569 19.9124 148.467 20.4911 147.089 20.4911C145.215 20.4911 143.699 19.7195 143.203 17.9009ZM143.148 15.1455C143.561 13.382 144.857 12.3625 146.758 12.3625C148.246 12.3625 149.734 13.1616 150.175 15.1455H143.148Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          d=\"M164.45 9.55192L161.088 19.196L157.754 9.55192H153.84L159.076 23.3292H163.127L168.363 9.55192H164.45Z\"\n          fill=\"#E2E8F0\"\n        />\n        <path\n          fill-rule=\"evenodd\"\n          clip-rule=\"evenodd\"\n          d=\"M8.32238 9.89169L13.6403 0.682007L26.8195 23.5069H0.461029L5.77893 14.2969L9.54072 16.4686L7.9849 19.1632H19.2957L13.6403 9.3691L12.0845 12.0637L8.32238 9.89169Z\"\n          fill=\"#E2E8F0\"\n        />\n      </svg>\n    </a>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/MoonIcon.tsx",
    "content": "import { motion } from \"framer-motion\";\nimport { transition } from \"../../utilities/animationConstants\";\n\nexport const MoonIcon = () => {\n  const variants = {\n    initial: { scale: 0.6, rotate: 90 },\n    animate: { scale: 1, rotate: 0, transition },\n    whileTap: { scale: 0.95, rotate: 15 },\n  };\n\n  return (\n    <motion.svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 50 50\"\n      key=\"moon\"\n    >\n      <motion.path\n        d=\"M 43.81 29.354 C 43.688 28.958 43.413 28.626 43.046 28.432 C 42.679 28.238 42.251 28.198 41.854 28.321 C 36.161 29.886 30.067 28.272 25.894 24.096 C 21.722 19.92 20.113 13.824 21.683 8.133 C 21.848 7.582 21.697 6.985 21.29 6.578 C 20.884 6.172 20.287 6.022 19.736 6.187 C 10.659 8.728 4.691 17.389 5.55 26.776 C 6.408 36.163 13.847 43.598 23.235 44.451 C 32.622 45.304 41.28 39.332 43.816 30.253 C 43.902 29.96 43.9 29.647 43.81 29.354 Z\"\n        fill=\"currentColor\"\n        initial=\"initial\"\n        animate=\"animate\"\n        whileTap=\"whileTap\"\n        variants={variants}\n      />\n    </motion.svg>\n  );\n};\n"
  },
  {
    "path": "app/components/Icons/ObjectIcon.tsx",
    "content": "export function ObjectIcon(props: React.SVGProps<SVGSVGElement>) {\n\n  return (\n    <svg\n    className={props.className}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 24 24\"\n      key=\"object\"\n    >\n      <path d=\"M8.63302 22C7.03731 21.9857 5.84052 21.4581 5.04267 20.4171C4.24481 19.3761 3.84589 17.836 3.84589 15.7968C3.84589 15.1408 3.75799 14.6132 3.58219 14.2139C3.40639 13.8004 3.12241 13.4724 2.73024 13.2299L2.5274 13.123C2.33807 13.0232 2.20284 12.9091 2.12171 12.7807C2.04057 12.6524 2 12.4884 2 12.2888V11.6898C2 11.4902 2.04057 11.3262 2.12171 11.1979C2.20284 11.0695 2.33807 10.9554 2.5274 10.8556L2.75052 10.7273C3.14269 10.4848 3.41991 10.164 3.58219 9.76471C3.75799 9.36542 3.84589 8.83779 3.84589 8.18182C3.84589 6.1426 4.24481 4.60963 5.04267 3.58289C5.84052 2.54189 7.03055 2.01426 8.61273 2H8.73444C8.85615 2 8.95757 2.04278 9.03871 2.12834C9.13337 2.2139 9.1807 2.32799 9.1807 2.47059V3.86096C9.1807 3.98931 9.14013 4.10339 9.05899 4.20321C8.97785 4.28877 8.87643 4.33155 8.75473 4.33155H8.67359C8.06505 4.37433 7.59175 4.53832 7.25368 4.82353C6.9156 5.10873 6.67895 5.5508 6.54372 6.14973C6.40849 6.7344 6.34087 7.53298 6.34087 8.54546C6.34087 9.40107 6.20564 10.1283 5.93518 10.7273C5.67825 11.3119 5.31313 11.7326 4.83982 11.9893C5.31313 12.2317 5.67825 12.6595 5.93518 13.2727C6.20564 13.8717 6.34087 14.5918 6.34087 15.4332C6.34087 16.4456 6.40849 17.2513 6.54372 17.8503C6.67895 18.4349 6.9156 18.8699 7.25368 19.1551C7.59175 19.4545 8.05829 19.6185 8.6533 19.6471H8.75473C8.87643 19.6613 8.97785 19.7112 9.05899 19.7968C9.14013 19.8824 9.1807 19.9893 9.1807 20.1176V21.5294C9.1807 21.6578 9.13337 21.7647 9.03871 21.8503C8.95757 21.9501 8.85615 22 8.73444 22H8.63302Z\" fill=\"currentColor\"/>\n      <path d=\"M15.367 2C16.9627 2.01426 18.1595 2.54189 18.9573 3.58289C19.7552 4.62389 20.1541 6.16399 20.1541 8.20321C20.1541 8.85918 20.242 9.39394 20.4178 9.80749C20.5936 10.2068 20.8776 10.5205 21.2698 10.7487L21.4726 10.8556C21.6619 10.9697 21.7972 11.0909 21.8783 11.2193C21.9594 11.3476 22 11.5045 22 11.6898V12.3102C22 12.4955 21.9594 12.6524 21.8783 12.7807C21.7972 12.9091 21.6619 13.0303 21.4726 13.1444L21.2495 13.2513C20.8708 13.4938 20.5936 13.8217 20.4178 14.2353C20.242 14.6346 20.1541 15.1622 20.1541 15.8182C20.1541 19.8966 18.5652 21.9572 15.3873 22H15.2656C15.1439 22 15.0357 21.9501 14.941 21.8503C14.8599 21.7647 14.8193 21.6578 14.8193 21.5294V20.1176C14.8193 19.9893 14.8599 19.8824 14.941 19.7968C15.0221 19.7112 15.1236 19.6613 15.2453 19.6471H15.3264C15.9349 19.6185 16.4083 19.4617 16.7463 19.1765C17.0844 18.8913 17.3211 18.4563 17.4563 17.8717C17.5915 17.2727 17.6591 16.467 17.6591 15.4545C17.6591 14.5847 17.7876 13.8574 18.0445 13.2727C18.315 12.6881 18.6869 12.2674 19.1602 12.0107C18.6869 11.754 18.315 11.3262 18.0445 10.7273C17.7876 10.1141 17.6591 9.38681 17.6591 8.54546C17.6591 7.53298 17.5915 6.7344 17.4563 6.14973C17.3211 5.5508 17.0844 5.10873 16.7463 4.82353C16.4083 4.53832 15.9417 4.38146 15.3467 4.35294L15.2453 4.33155C15.1371 4.33155 15.0357 4.28877 14.941 4.20321C14.8599 4.10339 14.8193 3.98931 14.8193 3.86096V2.47059C14.8193 2.32799 14.8599 2.2139 14.941 2.12834C15.0357 2.04278 15.1439 2 15.2656 2H15.367Z\" fill=\"currentColor\"/>\n  \n    </svg>\n  );\n};\n\n"
  },
  {
    "path": "app/components/Icons/ShortcutIcon.tsx",
    "content": "export type ShortcutIconProps = {\n  children: React.ReactNode;\n  className?: string;\n};\n\nexport function ShortcutIcon({ className, children }: ShortcutIconProps) {\n  return (\n    <span\n      className={`flex items-center justify-center rounded ${className ?? \"\"}`}\n    >\n      {children}\n    </span>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/SquareBracketsIcon.tsx",
    "content": "export function SquareBracketsIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"30\"\n      height=\"14\"\n      viewBox=\"0 0 30 14\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path d=\"M6 11V3H9V4.5H7.5V9.5H9V11H6Z\" fill=\"#0F172A\" />\n      <rect x=\"16\" width=\"14\" height=\"14\" rx=\"1.53846\" fill=\"currentColor\" />\n      <path\n        d=\"M25 3V11L21.9997 11V9.5H23.5V4.5H21.9997V3.00002L25 3Z\"\n        fill=\"#0F172A\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/StringIcon.tsx",
    "content": "export function StringIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      viewBox=\"-2 -5 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M4.536 6.845a3.908 3.908 0 0 1 2.598.566l-.025-.032a4.114 4.114 0 0 1 1.766 2.478 4.228 4.228 0 0 1-.391 3.047 4.026 4.026 0 0 1-2.33 1.92 3.898 3.898 0 0 1-2.973-.273h-.03a1.296 1.296 0 0 0-.082-.045c-.033-.018-.066-.035-.096-.056a4.236 4.236 0 0 1-.99-.88A7.746 7.746 0 0 1 .095 9.743a8.717 8.717 0 0 1 .191-3.498c.3-1.14.827-2.203 1.55-3.12A8.282 8.282 0 0 1 4.477.918 8.047 8.047 0 0 1 7.763 0c.287 0 .562.117.765.326.203.209.317.492.317.787 0 .296-.114.579-.317.788a1.066 1.066 0 0 1-.765.326c-.96.04-1.895.332-2.716.847A5.758 5.758 0 0 0 3.07 5.17a6.53 6.53 0 0 0-.905 2.906 3.962 3.962 0 0 1 2.37-1.232ZM15.53 6.83c.901-.12 1.815.079 2.591.565h-.006a4.105 4.105 0 0 1 1.761 2.473 4.22 4.22 0 0 1-.39 3.04 4.016 4.016 0 0 1-2.324 1.917 3.886 3.886 0 0 1-2.966-.273h-.036c-.03-.021-.063-.038-.097-.056a1.317 1.317 0 0 1-.08-.045 4.226 4.226 0 0 1-.987-.879 7.782 7.782 0 0 1-1.902-3.848 8.715 8.715 0 0 1 .194-3.49 8.564 8.564 0 0 1 1.546-3.111A8.276 8.276 0 0 1 15.469.92 8.037 8.037 0 0 1 18.742 0c.286 0 .56.117.763.325.202.209.316.491.316.786 0 .295-.114.577-.316.786a1.063 1.063 0 0 1-.763.325 5.526 5.526 0 0 0-2.707.846 5.738 5.738 0 0 0-1.968 2.092 6.519 6.519 0 0 0-.902 2.9 3.952 3.952 0 0 1 2.364-1.23Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/SunIcon.tsx",
    "content": "import { motion } from \"framer-motion\";\nimport { transition } from \"../../utilities/animationConstants\";\n\nexport const SunIcon = () => {\n  const whileTap = { scale: 0.95, rotate: 15 };\n\n  const raysVariants = {\n    initial: { rotate: 45 },\n    animate: { rotate: 0, transition },\n  };\n\n  const coreVariants = {\n    initial: { scale: 1.5 },\n    animate: { scale: 1, transition },\n  };\n\n  return (\n    <motion.svg\n      key=\"sun\"\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      whileTap={whileTap}\n      // Centers the rotation anchor point vertically & horizontally\n      style={{ originX: \"50%\", originY: \"50%\" }}\n    >\n      <motion.circle\n        cx=\"11.9998\"\n        cy=\"11.9998\"\n        r=\"5.75375\"\n        fill=\"currentColor\"\n        initial=\"initial\"\n        animate=\"animate\"\n        variants={coreVariants}\n      />\n      <motion.g initial=\"initial\" animate=\"animate\" variants={raysVariants}>\n        <circle\n          cx=\"3.08982\"\n          cy=\"6.85502\"\n          r=\"1.71143\"\n          transform=\"rotate(-60 3.08982 6.85502)\"\n          fill=\"currentColor\"\n        />\n        <circle\n          cx=\"3.0903\"\n          cy=\"17.1436\"\n          r=\"1.71143\"\n          transform=\"rotate(-120 3.0903 17.1436)\"\n          fill=\"currentColor\"\n        />\n        <circle cx=\"12\" cy=\"22.2881\" r=\"1.71143\" fill=\"currentColor\" />\n        <circle\n          cx=\"20.9101\"\n          cy=\"17.1436\"\n          r=\"1.71143\"\n          transform=\"rotate(-60 20.9101 17.1436)\"\n          fill=\"currentColor\"\n        />\n        <circle\n          cx=\"20.9101\"\n          cy=\"6.8555\"\n          r=\"1.71143\"\n          transform=\"rotate(-120 20.9101 6.8555)\"\n          fill=\"currentColor\"\n        />\n        <circle cx=\"12\" cy=\"1.71143\" r=\"1.71143\" fill=\"currentColor\" />\n      </motion.g>\n    </motion.svg>\n  );\n};\n"
  },
  {
    "path": "app/components/Icons/TreeIcon.tsx",
    "content": "export function TreeIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"28\"\n      height=\"30\"\n      viewBox=\"0 0 28 30\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M14.1805 30C14.5093 30 14.8245 29.8694 15.0571 29.6368C15.2895 29.4044 15.4201 29.089 15.4201 28.7602V22.5619H21.6184C23.2624 22.5619 24.839 21.9091 26.0014 20.7467C27.1638 19.5843 27.8167 18.0077 27.8167 16.3636C27.8234 14.3646 26.854 12.4882 25.2197 11.3368C25.2971 10.9512 25.3364 10.5588 25.3375 10.1653C25.3379 8.64671 24.7808 7.18076 23.7722 6.04565C22.7633 4.91048 21.3726 4.18522 19.8643 4.00752C19.2285 2.5124 18.0292 1.3282 16.5263 0.711248C15.0232 0.0942975 13.3379 0.0942975 11.8348 0.711248C10.332 1.3282 9.13259 2.51246 8.49682 4.00752C6.98853 4.18522 5.59785 4.91048 4.58897 6.04565C3.58025 7.18082 3.02318 8.64671 3.02362 10.1653C3.0247 10.5588 3.06405 10.9511 3.14144 11.3368C1.50714 12.4882 0.537772 14.3646 0.544468 16.3636C0.544468 18.0077 1.19734 19.5843 2.35974 20.7467C3.52214 21.9091 5.09872 22.5619 6.74276 22.5619H12.941V28.7602C12.941 29.089 13.0716 29.4044 13.304 29.6368C13.5366 29.8694 13.8518 30 14.1806 30H14.1805ZM6.74257 20.0827C5.75616 20.0827 4.81014 19.6908 4.11272 18.9934C3.41531 18.296 3.02337 17.35 3.02337 16.3636C3.02273 15.6644 3.22118 14.9796 3.59561 14.389C3.96981 13.7986 4.50443 13.3267 5.13716 13.029C5.41473 12.8941 5.63221 12.6606 5.7468 12.3742C5.86138 12.0875 5.86505 11.7685 5.75696 11.4794C5.31725 10.3533 5.4569 9.0835 6.13052 8.07999C6.80414 7.07625 7.92631 6.466 9.13497 6.4463C9.18145 6.4463 9.30857 6.46489 9.35202 6.46489C9.63457 6.47851 9.91302 6.39311 10.1391 6.22341C10.3655 6.05371 10.5255 5.8103 10.5916 5.53507C10.8614 4.46133 11.5979 3.56463 12.599 3.0914C13.6002 2.6184 14.7606 2.6184 15.7617 3.0914C16.7628 3.56461 17.4993 4.46133 17.7691 5.53507C17.8363 5.80962 17.9965 6.05239 18.2227 6.22186C18.4486 6.39135 18.7266 6.47739 19.0087 6.46485C19.083 6.46485 19.1544 6.46485 19.1388 6.44928L19.139 6.4495C20.3615 6.43934 21.5099 7.03579 22.2045 8.04207C22.8991 9.04841 23.0497 10.3333 22.607 11.473C22.4987 11.7621 22.5024 12.0811 22.6169 12.3678C22.7317 12.6545 22.949 12.8879 23.2268 13.0226C24.2385 13.5174 24.9716 14.444 25.2202 15.5424C25.4691 16.6408 25.2066 17.7928 24.5066 18.675C23.8066 19.5572 22.7445 20.0748 21.6182 20.0826H15.42V16.9151L19.8735 13.6424C20.1495 13.4519 20.3365 13.1579 20.3919 12.8272C20.4472 12.4965 20.3664 12.1575 20.1677 11.8875C19.969 11.6175 19.6692 11.4394 19.3371 11.3942C19.0049 11.3488 18.6685 11.4398 18.4045 11.6467L15.4199 13.8379V8.92563C15.4199 8.48267 15.1836 8.07347 14.8002 7.8521C14.4167 7.63074 13.9441 7.63074 13.5606 7.8521C13.1771 8.07347 12.9408 8.48272 12.9408 8.92563V15.1239L9.96256 12.9018C9.60716 12.6372 9.13741 12.5823 8.73054 12.7578C8.32368 12.9334 8.04136 13.3125 7.9899 13.7527C7.93844 14.1927 8.12565 14.627 8.48105 14.8916L12.9408 18.2045V20.0827L6.74257 20.0827Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Icons/TwitterIcon.tsx",
    "content": "export function TwitterIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"#F8FAFC\" />\n      <path\n        d=\"M5.43319 6H5.44396C5.50679 6.07822 5.56783 6.15853 5.63271 6.23467C6.27615 6.99369 6.98755 7.67709 7.80461 8.24238C8.73734 8.88746 9.75135 9.33515 10.8628 9.5487C11.3459 9.64122 11.8363 9.68896 12.3279 9.69132C12.3435 9.69076 12.359 9.68937 12.3744 9.68715C12.3664 9.65326 12.3582 9.62536 12.3538 9.59694C12.2968 9.24285 12.298 8.8816 12.3572 8.52789C12.5788 7.19811 13.6625 6.20938 14.9433 6.03181C15.0276 6.02008 15.1125 6.01069 15.1971 6H15.5621C15.5811 6.00425 15.6004 6.00747 15.6198 6.00965C15.9463 6.0339 16.2671 6.10973 16.5707 6.23441C16.9759 6.40128 17.347 6.64347 17.665 6.94858C17.6748 6.9577 17.6863 6.96472 17.6988 6.9692C17.7113 6.97368 17.7246 6.97554 17.7378 6.97465C18.2937 6.87178 18.8309 6.68327 19.3309 6.41562C19.4296 6.36347 19.5276 6.30924 19.6387 6.24901C19.3596 6.95666 18.9111 7.50057 18.2866 7.89872C18.8855 7.84345 19.4486 7.66119 20 7.42652C19.9379 7.51934 19.8738 7.60773 19.8071 7.6943C19.4338 8.17928 19.0121 8.61524 18.5141 8.96803C18.4917 8.98129 18.4736 9.00084 18.4618 9.02433C18.4501 9.04783 18.4453 9.07427 18.4479 9.10048C18.4686 9.8569 18.3891 10.6127 18.2115 11.3476C17.928 12.5242 17.41 13.6293 16.6897 14.5943C15.6526 15.9935 14.2124 17.0293 12.5695 17.5579C12.1008 17.7133 11.6191 17.8245 11.1303 17.8901C10.8077 17.9305 10.4838 17.9657 10.1596 17.9845C9.618 18.016 9.07662 17.996 8.53576 17.9558C8.04402 17.9215 7.55544 17.8504 7.07398 17.743C6.48868 17.6143 5.9222 17.4092 5.38857 17.1329C5.26034 17.0656 5.13699 16.9916 5.01132 16.9207L5.01517 16.9071H5.07672C5.59886 16.9102 6.12023 16.9071 6.63826 16.8229C7.15926 16.7393 7.66519 16.5776 8.13954 16.3431C8.63963 16.0944 9.09073 15.7695 9.53619 15.4334C9.54091 15.4282 9.54481 15.4223 9.54773 15.4159C9.1274 15.4081 8.75888 15.2882 8.4837 15.1716C7.99209 14.9607 7.53859 14.6678 7.14194 14.3049C6.77034 13.9696 6.44618 13.5941 6.21717 13.1417C6.15818 13.0254 6.11177 12.9026 6.05535 12.7736C6.52466 12.8779 6.96935 12.8317 7.40942 12.7071C5.75248 12.3707 4.93798 10.8409 5.00748 9.69654C5.43653 9.89705 5.89019 9.99691 6.35411 10.0566C6.22307 9.94633 6.08663 9.84568 5.96661 9.72757C5.13109 8.90571 4.83078 7.91854 5.09083 6.76267C5.15514 6.48825 5.27143 6.2292 5.43319 6Z\"\n        fill=\"#4338CA\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/IndentPreference.tsx",
    "content": "import React from \"react\";\nimport { Body } from \"./Primitives/Body\";\nimport { usePreferences } from \"~/components/PreferencesProvider\";\n\nconst MIN_INDENT = 1;\nconst MAX_INDENT = 8;\n\nexport function IndentPreference() {\n  const [preferences, setPreferences] = usePreferences();\n\n  const updatePreferences = (e: React.ChangeEvent<HTMLInputElement>) => {\n    let newIdent = Number(e.target.value);\n    if (newIdent < MIN_INDENT) newIdent = MIN_INDENT;\n    if (newIdent > MAX_INDENT) newIdent = MAX_INDENT;\n    e.target.value = newIdent.toString();\n    setPreferences({ ...preferences, indent: newIdent });\n  };\n\n  return (\n    <div className=\"flex items-center -mt-0.5\">\n      <label\n        className=\"pr-2 text-slate-800 transition dark:text-white\"\n        htmlFor=\"indent\"\n      >\n        <Body>Indent</Body>\n      </label>\n      <input\n        type=\"number\"\n        className=\"py-0 pr-0 pl-1 w-9 rounded-sm text-sm h-[23px] bg-slate-300 transition hover:bg-slate-400 hover:bg-opacity-50 dark:bg-slate-800 dark:text-slate-400 hover:cursor-pointer hover:dark:bg-slate-700 hover:dark:bg-opacity-70\"\n        defaultValue={preferences?.indent}\n        min={MIN_INDENT}\n        max={MAX_INDENT}\n        onChange={updatePreferences}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/InfoHeader.tsx",
    "content": "import { inferType } from \"@jsonhero/json-infer-types\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport { useJson } from \"~/hooks/useJson\";\nimport {\n  useJsonColumnViewAPI,\n  useJsonColumnViewState,\n} from \"~/hooks/useJsonColumnView\";\nimport { concatenated, getHierarchicalTypes } from \"~/utilities/dataType\";\nimport { formatRawValue } from \"~/utilities/formatter\";\nimport { isNullable } from \"~/utilities/nullable\";\nimport { CopyTextButton } from \"./CopyTextButton\";\nimport { Body } from \"./Primitives/Body\";\nimport { LargeMono } from \"./Primitives/LargeMono\";\nimport { Title } from \"./Primitives/Title\";\nimport { ValueIcon, ValueIconSize } from \"./ValueIcon\";\n\nexport type InfoHeaderProps = {\n  relatedPaths: string[];\n};\n\nexport function InfoHeader({ relatedPaths }: InfoHeaderProps) {\n  const { selectedNodeId, highlightedNodeId, selectedNodes } =\n    useJsonColumnViewState();\n  const { goToNodeId } = useJsonColumnViewAPI();\n\n  if (!selectedNodeId || !highlightedNodeId || selectedNodes.length === 0) {\n    return <EmptyState />;\n  }\n\n  const selectedNode = selectedNodes[selectedNodes.length - 1];\n\n  const [json] = useJson();\n\n  const selectedHeroPath = new JSONHeroPath(selectedNodeId);\n  const selectedJson = selectedHeroPath.first(json);\n  const selectedInfo = inferType(selectedJson);\n  const formattedSelectedInfo = formatRawValue(selectedInfo);\n  const selectedName = selectedNode.longTitle ?? selectedNode.title;\n\n  const isSelectedLeafNode =\n    selectedInfo.name !== \"object\" && selectedInfo.name !== \"array\";\n\n  const canBeNull = useMemo(() => {\n    return isNullable(relatedPaths, json);\n  }, [relatedPaths, json]);\n\n  const [hovering, setHovering] = useState(false);\n  console.warn(selectedInfo);\n\n  const newPath = formattedSelectedInfo.replace(/^#/, \"$\").replace(/\\//g, \".\");\n\n  const handleClick = useCallback(() => {\n    goToNodeId(newPath, \"pathBar\");\n  }, [newPath, goToNodeId]);\n\n  return (\n    <div className=\"mb-4 pb-4\">\n      <div className=\"flex items-center\">\n        <Title className=\"flex-1 mr-2 overflow-hidden overflow-ellipsis break-words text-slate-700 transition dark:text-slate-200\">\n          { selectedName ?? \"nothing\" }\n        </Title>\n        <div>\n          <ValueIcon\n            monochrome\n            type={selectedInfo}\n            size={ValueIconSize.Medium}\n          />\n        </div>\n      </div>\n      <div\n        className=\"relative w-full h-full\"\n        onMouseEnter={() => setHovering(true)}\n        onMouseLeave={() => setHovering(false)}\n      >\n        {isSelectedLeafNode && (\n          <LargeMono\n            className={`z-10 py-1 mb-1 text-slate-800 overflow-ellipsis break-words transition rounded-sm dark:text-slate-300 ${\n              hovering ? \"bg-slate-100 dark:bg-slate-700\" : \"bg-transparent\"\n            }`}\n          >\n            {selectedNode.name === \"$ref\" && checkPathExists(json, newPath) ? (\n              <button onClick={handleClick}>\n                {formatRawValue(selectedInfo)}\n              </button>\n            ) : (\n              formatRawValue(selectedInfo)\n            )}\n          </LargeMono>\n        )}\n        <div\n          className={`absolute top-1 right-0 flex justify-end h-full w-fit transition ${\n            hovering ? \"opacity-100\" : \"opacity-0\"\n          }`}\n        >\n          <CopyTextButton\n            className=\"bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-600 dark:hover:bg-slate-500\"\n            value={formatRawValue(selectedInfo)}\n          ></CopyTextButton>\n        </div>\n      </div>\n      <div className=\"flex text-gray-400\">\n        <Body className=\"flex-1\">\n          {concatenated(getHierarchicalTypes(selectedInfo))}\n        </Body>\n        {canBeNull && <Body>Can be null</Body>}\n      </div>\n    </div>\n  );\n}\n\nfunction checkPathExists(json: unknown, newPath: string) {\n  const heroPath = new JSONHeroPath(newPath);\n  const node = heroPath.first(json);\n  return Boolean(node);\n}\n\nfunction EmptyState() {\n  return (\n    <div className=\"mb-4 pb-4 border-b border-slate-300\">\n      <div className=\"flex items-center\">\n        <Title className=\"flex-1 mr-2 text-slate-800 transition dark:text-slate-300\">\n          Nothing selected\n        </Title>\n      </div>\n      <div>\n        <div>\n          <Title className=\"text-slate-800 mb-1 overflow-ellipsis break-words dark:text-slate-300\">\n            null\n          </Title>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/InfoPanel.tsx",
    "content": "import { PreviewValue } from \"./Preview/PreviewValue\";\nimport { RelatedValues } from \"./RelatedValues\";\nimport { PropertiesValue } from \"./Properties/PropertiesValue\";\nimport { InfoHeader } from \"./InfoHeader\";\nimport { ContainerInfo } from \"./ContainerInfo\";\nimport { useSelectedInfo } from \"~/hooks/useSelectedInfo\";\nimport { useRelatedPaths } from \"~/hooks/useRelatedPaths\";\nimport { useJsonDoc } from \"~/hooks/useJsonDoc\";\n\nexport function InfoPanel() {\n  const { minimal } = useJsonDoc();\n  const selectedInfo = useSelectedInfo();\n  const relatedPaths = useRelatedPaths();\n\n  if (!selectedInfo) {\n    return <></>;\n  }\n\n  return (\n    <>\n      <div\n        className={`${\n          minimal ? \"h-inspectorHeightMinimal\" : \"h-inspectorHeight\"\n        } p-4 bg-white border-l-[1px] border-slate-300 overflow-y-auto no-scrollbar transition dark:bg-slate-800 dark:border-slate-600`}\n      >\n        <InfoHeader relatedPaths={relatedPaths} />\n\n        <div className=\"mb-4\">\n          <PreviewValue />\n        </div>\n        <PropertiesValue />\n\n        <ContainerInfo />\n\n        <RelatedValues relatedPaths={relatedPaths} />\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/components/JsonColumnView.tsx",
    "content": "import {\n  useJsonColumnViewAPI,\n  useJsonColumnViewState,\n} from \"../hooks/useJsonColumnView\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\nimport { Columns } from \"./Columns\";\nimport { CopySelectedNodeShortcut } from \"./CopySelectedNode\";\n\nexport function JsonColumnView() {\n  const { getColumnViewProps, columns } = useJsonColumnViewState();\n\n  return (\n    <>\n      <KeyboardShortcuts />\n      <div {...getColumnViewProps()}>\n        <Columns columns={columns} />\n      </div>\n    </>\n  );\n}\n\nfunction KeyboardShortcuts() {\n  const api = useJsonColumnViewAPI();\n\n  useHotkeys(\n    \"down\",\n    (e) => {\n      e.preventDefault();\n      api.goToNextSibling();\n    },\n    { enabled: true },\n    [api]\n  );\n\n  useHotkeys(\n    \"up\",\n    (e) => {\n      e.preventDefault();\n      api.goToPreviousSibling();\n    },\n    [api]\n  );\n\n  useHotkeys(\n    \"right\",\n    (e) => {\n      e.preventDefault();\n      api.goToChildren();\n    },\n    [api]\n  );\n\n  useHotkeys(\n    \"left,alt+left\",\n    (e) => {\n      e.preventDefault();\n      api.goToParent({ source: e });\n    },\n    [api]\n  );\n\n  useHotkeys(\n    \"esc\",\n    (e) => {\n      e.preventDefault();\n      api.resetSelection();\n    },\n    [api]\n  );\n\n  return <>\n    <CopySelectedNodeShortcut />\n  </>;\n}\n"
  },
  {
    "path": "app/components/JsonEditor.tsx",
    "content": "import { CodeEditor } from \"./CodeEditor\";\nimport { useJson } from \"~/hooks/useJson\";\nimport { useCallback, useMemo, useRef } from \"react\";\nimport {\n  useJsonColumnViewAPI,\n  useJsonColumnViewState,\n} from \"~/hooks/useJsonColumnView\";\nimport { ViewUpdate } from \"@uiw/react-codemirror\";\nimport jsonMap from \"json-source-map\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\nimport {usePreferences} from '~/components/PreferencesProvider'\n\nexport function JsonEditor() {\n  const [json] = useJson();\n  const { selectedNodeId } = useJsonColumnViewState();\n  const { goToNodeId } = useJsonColumnViewAPI();\n  const [preferences] = usePreferences();\n\n  const jsonMapped = useMemo(() => {\n    return jsonMap.stringify(json, null, preferences?.indent || 2);\n  }, [json, preferences]);\n\n  const selection = useMemo<{ start: number; end: number } | undefined>(() => {\n    if (!selectedNodeId) {\n      return;\n    }\n\n    const path = new JSONHeroPath(selectedNodeId);\n    const pointer = path.jsonPointer();\n\n    const location = jsonMapped.pointers[pointer];\n\n    if (location) {\n      if (location.key) {\n        return { start: location.key.pos, end: location.valueEnd.pos };\n      }\n\n      return { start: location.value.pos, end: location.valueEnd.pos };\n    }\n  }, [selectedNodeId, jsonMapped]);\n\n  const currentSelectedLine = useRef<number | undefined>(undefined);\n\n  const onUpdate = useCallback(\n    (update: ViewUpdate) => {\n      if (!update.selectionSet) {\n        return;\n      }\n\n      const range = update.state.selection.ranges[0];\n      const line = update.state.doc.lineAt(range.anchor);\n\n      if (\n        currentSelectedLine.current &&\n        currentSelectedLine.current === line.number\n      ) {\n        return;\n      }\n\n      currentSelectedLine.current = line.number;\n\n      // Find the key if the selected line using jsonMapped.pointers\n      const pointerEntry = Object.entries(jsonMapped.pointers).find(\n        ([pointer, info]) => {\n          return info.value.line === line.number - 1;\n        }\n      );\n\n      if (!pointerEntry) {\n        return;\n      }\n\n      const [pointer] = pointerEntry;\n\n      const path = JSONHeroPath.fromPointer(pointer);\n\n      goToNodeId(path.toString(), \"editor\");\n    },\n    [goToNodeId]\n  );\n\n  return (\n    <CodeEditor\n      language=\"json\"\n      content={jsonMapped.json}\n      readOnly={true}\n      onUpdate={onUpdate}\n      selection={selection}\n    />\n  );\n}\n"
  },
  {
    "path": "app/components/JsonPreview.tsx",
    "content": "import { RangeSetBuilder } from \"@codemirror/rangeset\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\nimport {\n  useCodeMirror,\n  EditorView,\n  Decoration,\n  Facet,\n  ViewPlugin,\n  Compartment,\n  TransactionSpec,\n} from \"@uiw/react-codemirror\";\nimport jsonMap from \"json-source-map\";\nimport { useRef, useEffect, useMemo, useState } from \"react\";\nimport { getPreviewSetup } from \"~/utilities/codeMirrorSetup\";\nimport { lightTheme, darkTheme } from \"~/utilities/codeMirrorTheme\";\nimport { CopyTextButton } from \"./CopyTextButton\";\nimport { useTheme } from \"./ThemeProvider\";\nimport {usePreferences} from '~/components/PreferencesProvider';\nimport { useHotkeys } from \"react-hotkeys-hook\";\n\nexport type JsonPreviewProps = {\n  json: unknown;\n  highlightPath?: string;\n};\n\nexport function JsonPreview({ json, highlightPath }: JsonPreviewProps) {\n  const editor = useRef(null);\n  const [preferences] = usePreferences();\n\n  const jsonMapped = useMemo(() => {\n    return jsonMap.stringify(json, null, preferences?.indent || 2);\n  }, [json, preferences]);\n\n  const lines: LineRange | undefined = useMemo(() => {\n    if (!highlightPath) {\n      return;\n    }\n\n    let path = new JSONHeroPath(highlightPath);\n    let pointer = path.jsonPointer();\n\n    let selectionInfo = jsonMapped.pointers[pointer];\n\n    return {\n      from: selectionInfo.value.line + 1,\n      to: selectionInfo.valueEnd.line + 1,\n    };\n  }, [jsonMapped, highlightPath]);\n\n  const extensions = getPreviewSetup();\n\n  const highlighting = new Compartment();\n\n  if (lines) {\n    extensions.push(highlighting.of(highlightLineRange(lines)));\n  }\n\n  const [theme] = useTheme();\n\n  const { setContainer, view, state } = useCodeMirror({\n    container: editor.current,\n    extensions,\n    value: jsonMapped.json,\n    editable: false,\n    contentEditable: false,\n    autoFocus: false,\n    basicSetup: false,\n    theme: theme === \"light\" ? lightTheme() : darkTheme(),\n  });\n\n  useEffect(() => {\n    if (editor.current) {\n      setContainer(editor.current);\n    }\n  }, [editor.current]);\n\n  useEffect(() => {\n    if (!view) {\n      return;\n    }\n\n    let transactionSpec: TransactionSpec = {\n      changes: { from: 0, to: view.state.doc.length, insert: jsonMapped.json },\n    };\n\n    let range = lines;\n    if (range != null) {\n      transactionSpec.effects = highlighting.reconfigure(\n        highlightLineRange(range)\n      );\n    }\n\n    view.dispatch(transactionSpec);\n  }, [view, highlighting, jsonMapped, highlightPath]);\n\n  useHotkeys(\n    \"ctrl+a,meta+a,command+a\",\n    (e) => {\n      e.preventDefault();\n      view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });\n    },\n    [view, state]\n  );\n\n  const [hovering, setHovering] = useState(false);\n\n  return (\n    <div\n      className=\"relative w-full h-full\"\n      onMouseEnter={() => setHovering(true)}\n      onMouseLeave={() => setHovering(false)}\n    >\n      <div ref={editor} />\n      <div\n        className={`absolute top-1 right-0 flex justify-end w-full transition ${\n          hovering ? \"opacity-100\" : \"opacity-0\"\n        }`}\n      >\n        <CopyTextButton\n          value={jsonMapped.json}\n          className=\"bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-700 dark:hover:bg-slate-600\"\n        ></CopyTextButton>\n      </div>\n    </div>\n  );\n}\n\ninterface LineRange {\n  from: number;\n  to: number;\n}\n\nconst baseTheme = EditorView.baseTheme({\n  \"&light .cm-highlighted\": { backgroundColor: \"#ffee0055\" },\n  \"&dark .cm-highlighted\": { backgroundColor: \"#ffee0055\" },\n});\n\nconst highlightedRange = Facet.define<LineRange, LineRange>({\n  combine: (values) => (values.length ? values[0] : { from: -1, to: -1 }),\n});\n\nfunction highlightLineRange(range: LineRange | null) {\n  return [\n    baseTheme,\n    range == null ? [] : highlightedRange.of(range),\n    highlightLineRangePlugin,\n  ];\n}\nconst lineHighlightDecoration = Decoration.line({\n  attributes: { class: \"cm-highlighted\" },\n});\n\nfunction highlightLines(view: EditorView) {\n  let highlightRange = view.state.facet(highlightedRange);\n  let builder = new RangeSetBuilder();\n  for (let { from, to } of view.visibleRanges) {\n    for (let pos = from; pos <= to; ) {\n      let line = view.state.doc.lineAt(pos);\n      if (\n        line.number >= highlightRange.from &&\n        line.number <= highlightRange.to\n      ) {\n        builder.add(line.from, line.from, lineHighlightDecoration);\n      }\n      pos = line.to + 1;\n    }\n  }\n  return builder.finish();\n}\n\nconst highlightLineRangePlugin = ViewPlugin.fromClass(\n  class {\n    decorations: any;\n    constructor(view: any) {\n      this.decorations = highlightLines(view);\n    }\n\n    update(update: { docChanged: any; viewportChanged: any; view: any }) {\n      if (update.docChanged || update.viewportChanged)\n        this.decorations = highlightLines(update.view);\n    }\n  },\n  {\n    decorations: (v) => v.decorations,\n  }\n);\n"
  },
  {
    "path": "app/components/JsonSchemaViewer.tsx",
    "content": "import { JSONHeroPath } from \"@jsonhero/path\";\nimport { useMemo, useState } from \"react\";\nimport { useJsonSchema } from \"~/hooks/useJsonSchema\";\nimport { CodeViewer } from \"./CodeViewer\";\nimport { CopyTextButton } from \"./CopyTextButton\";\nimport {usePreferences} from '~/components/PreferencesProvider'\n\nexport function JsonSchemaViewer({ path }: { path: string }) {\n  const schema = useJsonSchema();\n  const schemaPath = schemaPathFromPath(path);\n  const schemaJson = schemaPath.first(schema);\n  const [hovering, setHovering] = useState(false);\n  const [preferences] = usePreferences();\n\n  const code = useMemo(() => {\n    return JSON.stringify(schemaJson, null, preferences?.indent || 2);\n  }, [schemaJson, preferences]);\n\n  return (\n    <div\n      className=\"relative w-full h-full\"\n      onMouseEnter={() => setHovering(true)}\n      onMouseLeave={() => setHovering(false)}\n    >\n      <CodeViewer code={code} lang=\"json\" />\n      <div\n        className={`absolute top-1 right-0 flex justify-end w-full transition ${\n          hovering ? \"opacity-100\" : \"opacity-0\"\n        }`}\n      >\n        <CopyTextButton\n          value={code}\n          className=\"bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-700 dark:hover:bg-slate-600\"\n        ></CopyTextButton>\n      </div>\n    </div>\n  );\n}\n\nfunction schemaPathFromPath(path: JSONHeroPath | string): JSONHeroPath {\n  const heroPath = typeof path === \"string\" ? new JSONHeroPath(path) : path;\n\n  if (heroPath.isRoot) {\n    return heroPath;\n  }\n\n  return heroPath.components.slice(1).reduce((acc, component) => {\n    if (component.isArray) {\n      return acc.child(\"items\");\n    } else {\n      return acc.child(\"properties\").child(component.toString());\n    }\n  }, new JSONHeroPath(\"$\"));\n}\n"
  },
  {
    "path": "app/components/JsonTreeView.tsx",
    "content": "import { ChevronDownIcon, ChevronRightIcon } from \"@heroicons/react/outline\";\nimport { useEffect, useRef } from \"react\";\nimport {\n  useJsonColumnViewAPI,\n  useJsonColumnViewState,\n} from \"~/hooks/useJsonColumnView\";\nimport { useJsonDoc } from \"~/hooks/useJsonDoc\";\nimport { JsonTreeViewNode, useJsonTreeViewContext } from \"~/hooks/useJsonTree\";\nimport { VirtualNode } from \"~/hooks/useVirtualTree\";\nimport { CopySelectedNodeShortcut } from \"./CopySelectedNode\";\nimport { Body } from \"./Primitives/Body\";\nimport { Mono } from \"./Primitives/Mono\";\n\nexport function JsonTreeView() {\n  const { selectedNodeId, selectedNodeSource } = useJsonColumnViewState();\n  const { goToNodeId } = useJsonColumnViewAPI();\n\n  const { tree, parentRef } = useJsonTreeViewContext();\n\n  // Scroll to the selected node when this component is first rendered.\n  const scrolledToNodeRef = useRef(false);\n\n  useEffect(() => {\n    if (!scrolledToNodeRef.current && selectedNodeId) {\n      tree.scrollToNode(selectedNodeId);\n      scrolledToNodeRef.current = true;\n    }\n  }, [selectedNodeId, scrolledToNodeRef]);\n\n  // Yup, this is hacky.\n  // This is to prevent the selection not changing the first time you try to move to a new node in the tree\n  const focusCount = useRef<number>(0);\n\n  // This focuses and scrolls to the selected node when the selectedNodeId\n  // is set from a source other than this tree (e.g. the search bar, path bar, related values).\n  useEffect(() => {\n    if (\n      tree.focusedNodeId &&\n      selectedNodeId &&\n      tree.focusedNodeId !== selectedNodeId\n    ) {\n      if (selectedNodeId === \"$\") {\n        return;\n      }\n\n      if (selectedNodeSource !== \"tree\" && focusCount.current > 0) {\n        focusCount.current = focusCount.current + 1;\n        tree.focusNode(selectedNodeId);\n        tree.scrollToNode(selectedNodeId);\n      }\n    }\n  }, [tree.focusedNodeId, goToNodeId, selectedNodeId, selectedNodeSource]);\n\n  // This is what syncs the tree view's focused node to the column view selected node\n  const previousFocusedNodeId = useRef<string | null>(null);\n\n  useEffect(() => {\n    let updated = false;\n\n    if (!previousFocusedNodeId.current) {\n      previousFocusedNodeId.current = tree.focusedNodeId;\n      updated = true;\n    }\n\n    if (\n      tree.focusedNodeId &&\n      (updated || previousFocusedNodeId.current !== tree.focusedNodeId)\n    ) {\n      previousFocusedNodeId.current = tree.focusedNodeId;\n      goToNodeId(tree.focusedNodeId, \"tree\");\n    }\n  }, [previousFocusedNodeId, tree.focusedNodeId, tree.focusNode, goToNodeId]);\n\n  const treeRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (treeRef.current) {\n      treeRef.current.focus({ preventScroll: true });\n    }\n  }, [treeRef.current]);\n\n  const { minimal } = useJsonDoc();\n\n  return (\n    <>\n      <CopySelectedNodeShortcut />\n      <div\n        className=\"text-white w-full\"\n        ref={parentRef}\n        style={{\n          height: `calc(100vh - ${minimal ? \"66px\" : \"106px\"})`,\n          overflowY: \"auto\",\n          overflowX: \"hidden\",\n        }}\n      >\n        <div\n          className=\"relative w-full outline-none\"\n          style={{ height: `${tree.totalSize}px` }}\n          {...tree.getTreeProps()}\n          ref={treeRef}\n        >\n          {tree.nodes.map((virtualNode) => (\n            <TreeViewNode\n              virtualNode={virtualNode}\n              key={virtualNode.node.id}\n              onToggle={(node, e) => tree.toggleNode(node.id, e)}\n              selectedNodeId={selectedNodeId}\n            />\n          ))}\n        </div>\n      </div>\n    </>\n  );\n}\n\nfunction TreeViewNode({\n  virtualNode,\n  onToggle,\n  selectedNodeId,\n}: {\n  virtualNode: VirtualNode<JsonTreeViewNode>;\n  selectedNodeId?: string;\n  onToggle?: (node: JsonTreeViewNode, e: MouseEvent) => void;\n}) {\n  const { node, virtualItem, depth } = virtualNode;\n\n  const indentClassName = computeTreeNodePaddingClass(depth);\n\n  const isSelected = selectedNodeId === node.id;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: 0,\n        left: 0,\n        width: \"100%\",\n        height: `${virtualNode.size}px`,\n        transform: `translateY(${virtualNode.start}px)`,\n      }}\n      key={virtualNode.node.id}\n      {...virtualNode.getItemProps()}\n    >\n      <div\n        className={`h-full flex pl-5 rounded-sm select-none ${\n          isSelected\n            ? \"bg-indigo-700\"\n            : virtualItem.index % 2\n            ? \"dark:bg-slate-900\"\n            : \"bg-slate-100 bg-opacity-90 dark:bg-slate-800 dark:bg-opacity-30\"\n        }`}\n      >\n        <div className={`pl-2 w-2/6 items-center flex`}>\n          {node.children && node.children.length > 0 && (\n            <span\n              onClick={(e) => {\n                if (onToggle) {\n                  e.preventDefault();\n                  onToggle(node, e.nativeEvent);\n                }\n              }}\n            >\n              {virtualNode.isCollapsed ? (\n                <ChevronRightIcon\n                  className={`w-4 h-4 mr-1 -ml-5  ${\n                    isSelected\n                      ? \"text-slate-100\"\n                      : \"text-slate-600 dark:text-slate-100\"\n                  }`}\n                />\n              ) : (\n                <ChevronDownIcon\n                  className={`w-4 h-4 mr-1 -ml-5 ${\n                    isSelected\n                      ? \"text-slate-100\"\n                      : \"text-slate-600 dark:text-slate-100\"\n                  }`}\n                />\n              )}\n            </span>\n          )}\n\n          <Body\n            className={`${indentClassName} leading-8 truncate whitespace-nowrap pl-2 pr-2 ${\n              isSelected\n                ? \"text-slate-100\"\n                : \"text-slate-700 dark:text-slate-200\"\n            }`}\n          >\n            {node.longTitle ?? node.name}\n          </Body>\n        </div>\n\n        <div className=\"flex w-4/6 items-center\">\n          <span className=\"mr-2\">\n            {node.icon && (\n              <node.icon\n                className={`h-5 w-5 ${\n                  isSelected\n                    ? \"text-slate-100\"\n                    : \"text-slate-400 dark:text-slate-500\"\n                }`}\n              />\n            )}\n          </span>\n          {node.subtitle && (\n            <Mono\n              className={`truncate pr-1 transition ${\n                isSelected\n                  ? \"text-slate-100\"\n                  : \"text-slate-500 dark:text-slate-200\"\n              }`}\n            >\n              {node.subtitle}\n            </Mono>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction computeTreeNodePaddingClass(depth: number) {\n  switch (depth) {\n    case 0:\n      return \"ml-[4px] border-l border-slate-400/70\";\n    case 1:\n      return \"ml-[calc(12px_+_4px)] border-l border-pink-400/70\";\n    case 2:\n      return \"ml-[calc(12px_*_2_+_4px)] border-l border-blue-400/70\";\n    case 3:\n      return \"ml-[calc(12px_*_3_+_4px)] border-l border-orange-400/70\";\n    case 4:\n      return \"ml-[calc(12px_*_4_+_4px)] border-l border-emerald-400/70\";\n    case 5:\n      return \"ml-[calc(12px_*_5_+_4px)] border-l border-pink-400/70\";\n    case 6:\n      return \"ml-[calc(12px_*_6_+_4px)] border-l border-blue-400/70\";\n    case 7:\n      return \"ml-[calc(12px_*_7_+_4px)] border-l border-orange-400/70\";\n    case 8:\n      return \"ml-[calc(12px_*_8_+_4px)] border-l border-emerald-400/70\";\n    case 9:\n      return \"ml-[calc(12px_*_9_+_4px)] border-l border-pink-400/70\";\n    case 10:\n      return \"ml-[calc(12px_*_10_+_4px)] border-l border-orange-400/70\";\n    default:\n      return \"ml-[calc(12px_*_11_+_4px)] border-l border-slate-400/70\";\n  }\n}\n"
  },
  {
    "path": "app/components/JsonView.tsx",
    "content": "import React from \"react\";\nimport { PathBar, PathHistoryControls } from \"./PathBar\";\nimport { SearchBar } from \"./SearchBar\";\n\nexport function JsonView({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"path-bar-and-column-wrapper flex flex-col flex-grow overflow-x-hidden border-l-[1px] border-slate-300 transition dark:border-slate-600\">\n      <div className=\"flex justify-between p-1 bg-slate-200 border-slate-300 border-b-[1px] transition dark:bg-slate-900 dark:border-slate-600\">\n        <div className=\"flex-shrink-0 flex-grow-0\">\n          <PathHistoryControls />\n        </div>\n        <div className=\"flex-1 pr-2 min-w-0\">\n          <PathBar />\n        </div>\n        <SearchBar />\n      </div>\n\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/NewDocument.tsx",
    "content": "import { DragAndDropForm } from \"./DragAndDropForm\";\nimport { Title } from \"./Primitives/Title\";\nimport { SampleUrls } from \"./SampleUrls\";\nimport { UrlForm } from \"./UrlForm\";\n\nexport function NewDocument() {\n  return (\n    <div className=\"bg-indigo-700 text-white rounded-sm shadow-md w-96 max-w-max p-3 transition\">\n      <div className=\"flex flex-col\">\n        <UrlForm className=\"mb-2\" />\n        <DragAndDropForm />\n\n        <div className=\"mt-4\">\n          <Title className=\"mb-2 text-slate-200\">No JSON? Try it out:</Title>\n          <SampleUrls />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/NewFile.tsx",
    "content": "import { DragAndDropForm } from \"./DragAndDropForm\";\nimport { Title } from \"./Primitives/Title\";\nimport { SampleUrls } from \"./SampleUrls\";\nimport { UrlForm } from \"./UrlForm\";\n\nexport function NewFile() {\n  return (\n    <div>\n      <div className=\"mb-4\">\n        <UrlForm />\n      </div>\n      <DragAndDropForm />\n\n      <div className=\"mt-4 pt-5\">\n        <Title className=\"mb-2 text-slate-200\">No JSON? Try it out:</Title>\n        <SampleUrls />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/OpenInWindow.tsx",
    "content": "export type OpenInNewWindowProps = {\n  children?: React.ReactNode;\n  url?: string;\n  className?: string;\n};\n\nexport function OpenInNewWindow({\n  url,\n  className,\n  children,\n}: OpenInNewWindowProps) {\n  return (\n    <a href={url} target=\"_blank\" className={`${className} relative`}>\n      {children}\n    </a>\n  );\n}\n"
  },
  {
    "path": "app/components/PathBar.tsx",
    "content": "import {\n  ChevronRightIcon,\n  ArrowLeftIcon,\n  ArrowRightIcon,\n  PencilAltIcon,\n  CheckIcon,\n} from \"@heroicons/react/outline\";\nimport { ColumnViewNode } from \"~/useColumnView\";\nimport { Body } from \"./Primitives/Body\";\nimport {\n  useJsonColumnViewAPI,\n  useJsonColumnViewState,\n} from \"../hooks/useJsonColumnView\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\nimport { memo, useEffect, useRef, useState } from \"react\";\nimport { useJson } from '~/hooks/useJson';\nimport { JSONHeroPath } from '@jsonhero/path';\n\nexport function PathBar() {\n  const [isEditable, setIsEditable] = useState(false);\n  const { selectedNodes, highlightedNodeId } = useJsonColumnViewState();\n  const { goToNodeId } = useJsonColumnViewAPI();\n  const [json] = useJson();\n\n  if (isEditable) {\n    return (\n      <PathBarText\n        selectedNodes={selectedNodes}\n        onConfirm={(newPath) => {\n          setIsEditable(false);\n          const heroPath = new JSONHeroPath(newPath);\n          const node = heroPath.first(json);\n          if (node) {\n            goToNodeId(newPath, 'pathBar');\n          }\n        }}\n      />\n    );\n  }\n\n  return (\n    <PathBarLink\n      selectedNodes={selectedNodes}\n      highlightedNodeId={highlightedNodeId}\n      enableEdit={() => setIsEditable(true)}\n    />\n  );\n}\n\nexport function PathBarText({ selectedNodes, onConfirm }: { selectedNodes: ColumnViewNode[], onConfirm: (newPath: string) => void; }) {\n  const [path, setPath] = useState('');\n  const ref = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    setPath(selectedNodes.at(-1)?.id || '');\n  }, [selectedNodes]);\n\n  useEffect(() => {\n    if (ref.current) {\n      ref.current.focus();\n    }\n  }, [ref]);\n\n  return (\n    <form\n      onSubmit={(e) => {\n        onConfirm(path);\n        e.preventDefault();\n      }}\n      // onBlur={() => onConfirm(path)}\n      className=\"flex overflow-x-hidden items-center bg-slate-300 dark:bg-slate-700 rounded-sm\"\n    >\n      <label className=\"grow\">\n        <input\n          ref={ref}\n          className={\n            \"w-full border-none outline-none text-ellipsis text-base px-2 py-0 rounded-sm bg-transparent dark:text-slate-200\"\n          }\n          style={{ boxShadow: 'none' }}\n          type=\"text\"\n          name=\"title\"\n          spellCheck=\"false\"\n          placeholder=\"Name your JSON file\"\n          value={path}\n          onChange={(e) => setPath(e.target.value)}\n        />\n      </label>\n      <button type=\"submit\" className=\"flex ml-auto justify-center items-center w-[26px] h-[26px] hover:bg-slate-400 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[10%]\">\n        <CheckIcon className='h-5' />\n      </button>\n    </form>\n  );\n}\n\nexport type PathBarLinkProps = {\n  selectedNodes: ColumnViewNode[];\n  highlightedNodeId?: string;\n  enableEdit: () => void;\n};\n\nexport function PathBarLink({\n  selectedNodes,\n  highlightedNodeId,\n  enableEdit,\n}: PathBarLinkProps) {\n  const { goToNodeId } = useJsonColumnViewAPI();\n\n  return (\n    <div\n      className=\"flex flex-shrink-0 flex-grow-0 overflow-x-hidden\"\n      onClick={(event) => {\n        if (event.detail == 2) {\n          enableEdit();\n        }\n      }}\n    >\n      {selectedNodes.map((node, index) => {\n        return (\n          <PathBarItem\n            key={index}\n            node={node}\n            isHighlighted={highlightedNodeId === node.id}\n            onClick={(id) => goToNodeId(id, \"pathBar\")}\n            isLast={index == selectedNodes.length - 1}\n          />\n        );\n      })}\n      <button\n        className=\"flex ml-auto justify-center items-center w-[26px] h-[26px] hover:bg-slate-300 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[10%]\"\n        onClick={enableEdit}>\n        <PencilAltIcon className='h-5' />\n      </button>\n    </div>\n  );\n}\n\nexport function PathHistoryControls() {\n  const { canGoBack, canGoForward } = useJsonColumnViewState();\n  const { goBack, goForward } = useJsonColumnViewAPI();\n\n  useHotkeys(\n    \"[\",\n    () => {\n      goBack();\n    },\n    [goBack]\n  );\n\n  useHotkeys(\n    \"]\",\n    () => {\n      goForward();\n    },\n    [goForward]\n  );\n\n  return (\n    <div className=\"flex h-full\">\n      <button\n        className=\"flex justify-center items-center w-[26px] h-[26px] disabled:text-slate-400 disabled:text-opacity-50 text-slate-700 hover:bg-slate-300 hover:disabled:bg-transparent rounded-sm transition dark:disabled:text-slate-700 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:hover:disabled:bg-transparent\"\n        disabled={!canGoBack}\n        onClick={goBack}\n      >\n        <ArrowLeftIcon className=\"w-5 h-6\" />\n      </button>\n      <button\n        className=\"flex justify-center items-center w-[26px] h-[26px] disabled:text-slate-400 disabled:text-opacity-50 text-slate-700 hover:bg-slate-300 hover:disabled:bg-transparent rounded-sm transition dark:disabled:text-slate-700 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:hover:disabled:bg-transparent\"\n        disabled={!canGoForward}\n        onClick={goForward}\n      >\n        <ArrowRightIcon className=\"w-5 h-6\" />\n      </button>\n    </div>\n  );\n}\n\nfunction PathBarElement({\n  node,\n  isHighlighted,\n  onClick,\n  isLast,\n}: {\n  node: ColumnViewNode;\n  isHighlighted: boolean;\n  onClick?: (id: string) => void;\n  isLast: boolean;\n}) {\n  return (\n    <div\n      className=\"flex items-center min-w-0\"\n      style={{\n        flexShrink: 1,\n      }}\n    >\n      <div\n        className={`flex items-center hover:cursor-pointer min-w-0 transition ${isHighlighted\n          ? \"text-slate-700 bg-slate-300 px-2 py-[3px] rounded-sm dark:text-white dark:bg-slate-700\"\n          : \"hover:bg-slate-300 px-2 py-[3px] rounded-sm transition dark:hover:bg-white dark:hover:bg-opacity-[5%]\"\n          }`}\n        style={{\n          flexShrink: 1,\n        }}\n        onClick={() => onClick && onClick(node.id)}\n      >\n        <div className=\"w-4 flex-shrink-[0.5] flex-grow-0 flex-col justify-items-center whitespace-nowrap overflow-x-hidden transition dark:text-slate-400\">\n          {node.icon && <node.icon className=\"h-3 w-3\" />}\n        </div>\n        <Body className=\"flex-shrink flex-grow-0 whitespace-nowrap overflow-x-hidden text-ellipsis transition dark:text-slate-400\">\n          {node.title}\n        </Body>\n      </div>\n\n      {isLast ? (\n        <></>\n      ) : (\n        <ChevronRightIcon className=\"flex-grow-0 flex-shrink-[0.5] w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden\" />\n      )}\n    </div>\n  );\n}\n\nconst PathBarItem = memo(PathBarElement);\n"
  },
  {
    "path": "app/components/PathPreview.tsx",
    "content": "import { ChevronRightIcon, EyeIcon } from \"@heroicons/react/outline\";\nimport { useMemo } from \"react\";\nimport { useJsonColumnViewAPI } from \"~/hooks/useJsonColumnView\";\nimport { ColumnViewNode, IconComponent } from \"~/useColumnView\";\nimport { Body } from \"./Primitives/Body\";\n\nimport eyeIcon from \"~/assets/svgs/EyeIcon.svg\";\n\nexport type PathPreviewProps = {\n  nodes: ColumnViewNode[];\n  maxComponents?: number;\n  enabled?: boolean;\n};\n\ntype ValueComponent = {\n  type: \"value\";\n  id: string;\n  title: string;\n  icon?: IconComponent;\n};\n\ntype EllipsisComponent = {\n  type: \"ellipsis\";\n  id: \"ellipsis\";\n};\n\ntype Component = ValueComponent | EllipsisComponent;\n\nexport function PathPreview({\n  nodes,\n  maxComponents,\n  enabled,\n}: PathPreviewProps) {\n  const isEnabled = useMemo(() => {\n    if (enabled === undefined) {\n      return true;\n    }\n    return enabled;\n  }, [enabled]);\n\n  const { goToNodeId } = useJsonColumnViewAPI();\n\n  const components = useMemo<Array<Component>>(() => {\n    if (maxComponents == null || nodes.length <= maxComponents) {\n      return nodes.map((n) => {\n        return { type: \"value\", id: n.id, title: n.title, icon: n.icon };\n      });\n    }\n\n    let components = Array<Component>();\n\n    //add the elements up to the ellipsis\n    for (let index = 0; index < maxComponents - 1; index++) {\n      const node = nodes[index];\n      components.push({\n        type: \"value\",\n        id: node.id,\n        title: node.title,\n        icon: node.icon,\n      });\n    }\n\n    //add ellipsis\n    components.push({ type: \"ellipsis\", id: \"ellipsis\" });\n\n    //add final element\n    const lastNode = nodes[nodes.length - 1];\n    components.push({\n      type: \"value\",\n      id: lastNode.id,\n      title: lastNode.title,\n      icon: lastNode.icon,\n    });\n\n    return components;\n  }, [nodes, maxComponents]);\n\n  return (\n    <div\n      className={`flex select-none pl-7 ${\n        isEnabled\n          ? `relative transition hover:bg-slate-200 hover:cursor-pointer dark:hover:bg-slate-600 after:transition after:absolute after:h-3 after:w-3 after:opacity-0 hover:after:opacity-100 after:top-1 after:left-1 after:content-[''] after:bg-[url('${eyeIcon}')] after:bg-no-repeat`\n          : \"disabled\"\n      }`}\n      onClick={() =>\n        isEnabled &&\n        goToNodeId(components[components.length - 1].id, \"relatedValues\")\n      }\n    >\n      <div\n        className={`flex rounded-sm px-2 ${\n          isEnabled\n            ? \"\"\n            : \"hover:bg-slate-100 hover:cursor-pointer dark:hover:bg-slate-600\"\n        }`}\n      >\n        {components.map((node, index) => {\n          if (node.type === \"ellipsis\") {\n            return (\n              <div\n                key={node.id}\n                className=\"flex flex-none items-center min-w-0\"\n              >\n                <div className=\"flex-none text-md\">…</div>\n                <ChevronRightIcon className=\"flex-none w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden\" />\n              </div>\n            );\n          } else {\n            return (\n              <div className=\"flex items-center min-w-0\" key={node.id}>\n                <div className=\"flex items-center min-w-0\">\n                  <div className=\"w-4 flex-shrink-[0.5] flex-grow-0 flex-col justify-items-center whitespace-nowrap overflow-x-hidden transition dark:text-slate-300\">\n                    {node.icon && <node.icon className=\"h-3 w-3\" />}\n                  </div>\n                  <Body className=\"flex-shrink flex-grow-0 whitespace-nowrap overflow-x-hidden text-ellipsis transition dark:text-slate-300\">\n                    {node.title}\n                  </Body>\n                </div>\n\n                {index == components.length - 1 ? (\n                  <></>\n                ) : (\n                  <ChevronRightIcon className=\"flex-grow-0 flex-shrink-[0.5] w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden\" />\n                )}\n              </div>\n            );\n          }\n        })}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/PreferencesProvider.tsx",
    "content": "import {createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState} from 'react';\n\ninterface Preferences {\n  indent: number;\n}\n\nconst PreferencesDefaults: Preferences = {\n  indent: 2,\n};\n\ntype PreferencesContextType = [\n  Preferences | undefined,\n  Dispatch<SetStateAction<Preferences | undefined>>\n];\n\nconst PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);\n\nconst loadPreferences = (): Preferences => {\n  const savedPreferences = localStorage.getItem('preferences');\n  const parsedPreferences = JSON.parse(savedPreferences || '{}');\n  for (const [key, value] of Object.entries(PreferencesDefaults)) {\n    if (!parsedPreferences[key]) parsedPreferences[key] = value;\n  }\n  return parsedPreferences;\n};\nconst savePreferences = (preferences: Preferences) => localStorage.setItem('preferences', JSON.stringify(preferences));\n\nexport function PreferencesProvider({\n  children,\n}: {\n  children: ReactNode;\n}) {\n  const [preferences, setPreferences] = useState<Preferences>();\n\n  useEffect(() => {\n    const preferences = loadPreferences();\n    setPreferences(preferences);\n  }, []);\n\n  useEffect(() => {\n    if (preferences === undefined) return;\n    savePreferences(preferences);\n  }, [preferences]);\n\n  return (\n    <PreferencesContext.Provider value={[preferences, setPreferences]}>\n      {children}\n    </PreferencesContext.Provider>\n  );\n}\n\nexport function usePreferences() {\n  const context = useContext(PreferencesContext);\n  if (context === undefined) {\n    throw new Error('usePreferences must be used within a PreferencesProvider');\n  }\n  return context;\n}"
  },
  {
    "path": "app/components/Preview/CalendarMonth.tsx",
    "content": "import { useMemo } from \"react\";\nimport { Title } from \"../Primitives/Title\";\n\nexport type CalendarMonthProps = {\n  date: Date;\n};\n\ntype Day = {\n  date: string;\n  isCurrentMonth: boolean;\n  isHighlighted: boolean;\n};\n\nfunction dateString(date: Date): string {\n  return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;\n}\n\nfunction isSameDay(date: Date, otherDate: Date): boolean {\n  return (\n    date.getFullYear() === otherDate.getFullYear() &&\n    date.getMonth() === otherDate.getMonth() &&\n    date.getDate() === otherDate.getDate()\n  );\n}\n\nexport function CalendarMonth({ date }: CalendarMonthProps) {\n  const days = useMemo<Array<Day>>(() => {\n    let days: Array<Day> = [];\n\n    //create first day of the month\n    const firstDayOfMonth = new Date(date);\n    firstDayOfMonth.setDate(1);\n\n    //if the first day isn't a monday, we need to add days from the previous month in\n    const firstDayOfWeek = firstDayOfMonth.getDay() - 1;\n    if (firstDayOfWeek != 0) {\n      const previousMonthDate = new Date(firstDayOfMonth);\n      for (let index = 0; index < firstDayOfWeek; index++) {\n        previousMonthDate.setDate(previousMonthDate.getDate() - 1);\n        days.push({\n          date: dateString(previousMonthDate),\n          isCurrentMonth: false,\n          isHighlighted: isSameDay(date, previousMonthDate),\n        });\n      }\n    }\n\n    //current month\n    let currentMonthDate = new Date(firstDayOfMonth);\n    const monthNumber = firstDayOfMonth.getMonth();\n    while (true) {\n      days.push({\n        date: dateString(currentMonthDate),\n        isCurrentMonth: true,\n        isHighlighted: isSameDay(date, currentMonthDate),\n      });\n\n      currentMonthDate.setDate(currentMonthDate.getDate() + 1);\n\n      if (currentMonthDate.getMonth() !== monthNumber) {\n        break;\n      }\n    }\n\n    //next month\n    const lastDayOfMonthDayOfWeek = currentMonthDate.getDay() - 1;\n    const nextMonthDayCount = 7 - lastDayOfMonthDayOfWeek;\n    for (let index = 0; index < nextMonthDayCount; index++) {\n      days.push({\n        date: dateString(currentMonthDate),\n        isCurrentMonth: false,\n        isHighlighted: isSameDay(date, currentMonthDate),\n      });\n      currentMonthDate.setDate(currentMonthDate.getDate() + 1);\n    }\n\n    return days;\n  }, [date]);\n\n  return (\n    <section className=\"\">\n      <Title className=\"text-left text-slate-800 dark:text-slate-400\">\n        {new Intl.DateTimeFormat(\"en-US\", {\n          weekday: \"short\",\n          year: \"numeric\",\n          month: \"short\",\n          day: \"numeric\",\n          hour: \"numeric\",\n          hour12: true,\n          minute: \"2-digit\",\n          second: \"2-digit\",\n          timeZoneName: \"short\",\n        }).format(date)}\n      </Title>\n      <div className=\"uppercase mt-2 grid text-center tracking-wider grid-cols-7 text-sm leading-6 text-gray-500 dark:text-slate-500\">\n        <div>Mon</div>\n        <div>Tue</div>\n        <div>Wed</div>\n        <div>Thu</div>\n        <div>Fri</div>\n        <div>Sat</div>\n        <div>Sun</div>\n      </div>\n      <div className=\"isolate mt-2 grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm ring-1 cursor-default ring-slate-200 dark:ring-slate-600 dark:bg-slate-600\">\n        {days.map((day, dayIdx) => (\n          <button\n            key={day.date}\n            type=\"button\"\n            className={`\"cursor-default\" ${classNames(\n              day.isCurrentMonth\n                ? \"bg-white text-slate-900 dark:text-slate-300 dark:bg-slate-800\"\n                : \"bg-slate-100 text-slate-400 dark:text-slate-500 dark:bg-slate-800 dark:bg-opacity-90\",\n              dayIdx === 0 && \"rounded-tl-lg\",\n              dayIdx === 6 && \"rounded-tr-lg\",\n              dayIdx === days.length - 7 && \"rounded-bl-lg\",\n              dayIdx === days.length - 1 && \"rounded-br-lg\",\n              \"relative py-1.5 focus:z-10\"\n            )}`}\n          >\n            <time\n              dateTime={day.date}\n              className={classNames(\n                day.isHighlighted && \"bg-indigo-600 font-semibold text-white\",\n                \"mx-auto flex h-7 w-7 items-center cursor-default justify-center rounded-full\"\n              )}\n            >\n              {day.date.split(\"-\").pop()?.replace(/^0/, \"\")}\n            </time>\n          </button>\n        ))}\n      </div>\n    </section>\n  );\n}\n\nfunction classNames(...classes: (string | boolean)[]) {\n  return classes.filter(Boolean).join(\" \");\n}\n"
  },
  {
    "path": "app/components/Preview/PreviewBox.tsx",
    "content": "import { useCallback } from \"react\";\nimport { Title } from \"../Primitives/Title\";\n\nexport type PreviewBoxProps = {\n  link?: string;\n  children: React.ReactNode;\n  className?: string;\n};\n\nexport function PreviewBox({ link, className, children }: PreviewBoxProps) {\n  const onClick = useCallback(() => {\n    if (!link) return;\n    window.open(link, \"_blank\");\n  }, [link]);\n\n  return (\n    <div className={className}>\n      <Title className=\"text-slate-700 transition dark:text-slate-400 mb-2\">\n        Preview\n      </Title>\n      <div\n        onClick={onClick}\n        className=\"block rounded-sm p-2 text-slate-700 bg-slate-100 transition dark:text-slate-300 dark:bg-white dark:bg-opacity-5 hover:cursor-pointer\"\n      >\n        <div>{children}</div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/PreviewProperties.tsx",
    "content": "import { Body } from \"../Primitives/Body\";\n\nexport type PreviewPropertyProps = {\n  properties: Array<PreviewProperty>;\n};\n\nexport type PreviewProperty = {\n  key: string;\n  title: string;\n  icon?: JSX.Element;\n};\n\nexport function PreviewProperties({ properties }: PreviewPropertyProps) {\n  return (\n    <div className=\"-mb-1\">\n      {properties.map((property) => {\n        return (\n          <Body\n            className=\"text-slate-500 mr-2 inline-flex items-center\"\n            key={property.key}\n          >\n            {property.icon && (\n              <span className=\"w-4 h-4 inline-block text-slate-500 mr-1 flex-none\">\n                {property.icon}\n              </span>\n            )}\n            <span>{property.title}</span>\n          </Body>\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/PreviewValue.tsx",
    "content": "import { useSelectedInfo } from \"~/hooks/useSelectedInfo\";\nimport { PreviewString } from \"./Types/PreviewString\";\n\nexport function PreviewValue() {\n  const info = useSelectedInfo();\n\n  if (!info) {\n    return <></>;\n  }\n\n  switch (info.name) {\n    case \"string\":\n      return <PreviewString info={info} />;\n    default:\n      return <></>;\n  }\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewAudioUri.tsx",
    "content": "import { useRef } from \"react\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\nimport { Body } from \"~/components/Primitives/Body\";\nimport { PreviewBox } from \"../PreviewBox\";\n\nexport function PreviewAudioUri({\n  src,\n  contentType,\n}: {\n  src: string;\n  contentType: string;\n}) {\n  const mediaRef = useRef<HTMLMediaElement>(null);\n\n  useHotkeys(\n    \"space\",\n    (e) => {\n      e.preventDefault();\n\n      if (mediaRef.current) {\n        if (mediaRef.current.paused) {\n          mediaRef.current.play();\n        } else {\n          mediaRef.current.pause();\n        }\n      }\n    },\n    [mediaRef]\n  );\n\n  return (\n    <div>\n      <PreviewBox>\n        <Body>\n          <audio controls src={src} ref={mediaRef}>\n            Sorry, your browser doesn't support embedded audio.\n          </audio>\n        </Body>\n      </PreviewBox>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewDate.tsx",
    "content": "import { Temporal } from \"@js-temporal/polyfill\";\nimport { JSONDateTimeFormat, JSONStringType } from \"@jsonhero/json-infer-types\";\nimport { useMemo } from \"react\";\nimport { inferTemporal } from \"~/utilities/inferredTemporal\";\nimport { CalendarMonth } from \"../CalendarMonth\";\n\nexport type PreviewDateProps = {\n  value: string;\n  format: JSONDateTimeFormat;\n};\n\nexport function PreviewDate({ value, format }: PreviewDateProps) {\n  const temporal = inferTemporal(value, format);\n\n  if (!temporal) {\n    return <></>;\n  }\n\n  // Can only convert to the legacy Date class if temporal is either a ZonedDateTime or an Instant\n  if (\"epochMilliseconds\" in temporal) {\n    const date = new Date(temporal.epochMilliseconds);\n\n    return <CalendarMonth date={date} />;\n  } else if (temporal instanceof Temporal.PlainDate) {\n    const date = new Date(temporal.year, temporal.month - 1, temporal.day);\n\n    return <CalendarMonth date={date} />;\n  } else if (temporal instanceof Temporal.PlainDateTime) {\n    const date = new Date(\n      temporal.year,\n      temporal.month - 1,\n      temporal.day,\n      temporal.hour,\n      temporal.minute,\n      temporal.second,\n      temporal.millisecond\n    );\n\n    return <CalendarMonth date={date} />;\n  } else {\n    return <></>;\n  }\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewHtml.tsx",
    "content": "import { Body } from \"~/components/Primitives/Body\";\nimport { Title } from \"~/components/Primitives/Title\";\nimport { PreviewBox } from \"../PreviewBox\";\nimport { PreviewHtml } from \"./preview.types\";\n\nexport type PreviewHtmlProps = {\n  info: PreviewHtml;\n};\n\nexport function PreviewHtml({ info }: PreviewHtmlProps) {\n  return (\n    <PreviewBox link={info.url}>\n      <div>\n        {info.title && (\n          <Title>\n            {info.icon && (\n              <img src={info.icon.url} className=\"w-4 h-4 inline mr-1\" alt=\"\" />\n            )}\n            <span className=\"inline\">{info.title}</span>\n          </Title>\n        )}\n        {info.description && <Body>{info.description}</Body>}\n      </div>\n      {info.image && (\n        <div>\n          <img className=\"block\" src={info.image?.url} alt={info.image?.alt} />\n        </div>\n      )}\n    </PreviewBox>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewIPFSImage.tsx",
    "content": "import { PreviewImageUri } from \"./PreviewImageUri\";\n\nexport function PreviewIPFSImage({ src }: { src: URL }) {\n  const newSrc = createPreviewIPFSImageURL(src);\n\n  return <PreviewImageUri src={newSrc} />;\n}\n\nconst createPreviewIPFSImageURL = (src: URL): string => {\n  const url = new URL(src.href);\n\n  url.protocol = \"https:\";\n  url.pathname = `/ipfs/${src.pathname.substring(1)}`;\n  url.hostname = \"ipfs.io\";\n  // Remove the leading slash\n\n  return url.href;\n};\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewImage.tsx",
    "content": "import { formatBytes } from \"~/utilities/formatter\";\nimport { PreviewBox } from \"../PreviewBox\";\nimport { PreviewProperties, PreviewProperty } from \"../PreviewProperties\";\nimport { PreviewImage } from \"./preview.types\";\n\nexport type PreviewImageProps = {\n  info: PreviewImage;\n};\n\nexport function PreviewImage({ info }: PreviewImageProps) {\n  let properties: Array<PreviewProperty> = [];\n\n  if (info.mimeType) {\n    properties.push({ key: \"mimeType\", title: info.mimeType });\n  }\n\n  if (info.size) {\n    properties.push({ key: \"fileSize\", title: `${formatBytes(info.size)}` });\n  }\n\n  const src = info.image ? info.image.url : info.url;\n\n  return (\n    <PreviewBox link={info.url}>\n      <img className=\"block max-h-96 w-full object-contain\" src={src} />\n      <PreviewProperties properties={properties} />\n    </PreviewBox>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewImageUri.tsx",
    "content": "import { Body } from \"~/components/Primitives/Body\";\nimport { PreviewBox } from \"../PreviewBox\";\n\nexport function PreviewImageUri({\n  src,\n  contentType,\n  alt = \"\",\n}: {\n  src: string;\n  contentType?: string;\n  alt?: string\n}) {\n  return (\n    <div>\n      <PreviewBox link={src}>\n        <Body>\n          <img src={src} alt={alt} />\n        </Body>\n      </PreviewBox>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewJson.tsx",
    "content": "import { useState } from \"react\";\nimport { CodeViewer } from \"~/components/CodeViewer\";\nimport { CopyTextButton } from \"~/components/CopyTextButton\";\nimport { OpenInNewWindow } from \"~/components/OpenInWindow\";\nimport { Body } from \"~/components/Primitives/Body\";\nimport { PreviewBox } from \"../PreviewBox\";\nimport { PreviewJson } from \"./preview.types\";\n\nexport function PreviewJson({ preview }: { preview: PreviewJson }) {\n  const [hovering, setHovering] = useState(false);\n  const jsonHeroUrl = new URL(\n    `/actions/createFromUrl?jsonUrl=${encodeURIComponent(preview.url)}`,\n    window.location.origin\n  );\n\n  jsonHeroUrl.searchParams.append(\"utm_source\", \"preview\");\n\n  const code = JSON.stringify(preview.json, null, 2);\n\n  return (\n    <PreviewBox className=\"relative\">\n      <div\n        className=\"relative w-full h-full\"\n        onMouseEnter={() => setHovering(true)}\n        onMouseLeave={() => setHovering(false)}\n      >\n        <CodeViewer code={code} />\n        <div\n          className={`absolute top-0 flex justify-end pt-1 pr-1 w-full transition ${\n            hovering ? \"opacity-100\" : \"opacity-0\"\n          }`}\n        >\n          <CopyTextButton\n            value={code}\n            className=\"bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition dark:text-white dark:bg-slate-700 dark:hover:bg-slate-600\"\n          ></CopyTextButton>\n          <OpenInNewWindow\n            url={jsonHeroUrl.href}\n            className=\"bg-slate-200 hover:bg-slate-300 h-fit px-2 py-0.5 rounded-sm transition dark:text-white dark:bg-slate-700 dark:hover:bg-slate-600\"\n          >\n            <Body>Open in tab</Body>\n          </OpenInNewWindow>\n        </div>\n      </div>\n    </PreviewBox>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewString.tsx",
    "content": "import { JSONStringType } from \"@jsonhero/json-infer-types/lib/@types\";\nimport {\n  JSONColorFormat,\n  JSONJSONFormat,\n} from \"@jsonhero/json-infer-types/lib/formats\";\nimport Color from \"color\";\nimport { CodeViewer } from \"~/components/CodeViewer\";\nimport { PreviewBox } from \"../PreviewBox\";\nimport { PreviewAudioUri } from \"./PreviewAudioUri\";\nimport { PreviewDate } from \"./PreviewDate\";\nimport { PreviewImageUri } from \"./PreviewImageUri\";\nimport { PreviewIPFSImage } from \"./PreviewIPFSImage\";\nimport { PreviewUri } from \"./PreviewUri\";\nimport { PreviewVideoUri } from \"./PreviewVideoUri\";\n\nexport function PreviewString({ info }: { info: JSONStringType }) {\n  if (info.format == null) {\n    return <></>;\n  }\n\n  switch (info.format.name) {\n    case \"uri\":\n      if (\n        info.format.contentType === \"image/png\" ||\n        info.format.contentType === \"image/jpeg\" ||\n        info.format.contentType === \"image/gif\" ||\n        info.format.contentType === \"image/svg+xml\" ||\n        info.format.contentType === \"image/webp\"\n      ) {\n        const url = new URL(info.value);\n\n        if (url.protocol === \"ipfs:\") {\n          return <PreviewIPFSImage src={url} />;\n        } else {\n          return (\n            <PreviewImageUri\n              src={info.value}\n              contentType={info.format.contentType}\n            />\n          );\n        }\n      } else if (\n        info.format.contentType === \"video/mp4\" ||\n        info.format.contentType === \"video/webm\" ||\n        info.format.contentType === \"video/ogg\"\n      ) {\n        return (\n          <PreviewVideoUri\n            src={info.value}\n            contentType={info.format.contentType}\n          />\n        );\n      } else if (\n        info.format.contentType === \"audio/mpeg\" ||\n        info.format.contentType === \"audio/ogg\" ||\n        info.format.contentType === \"audio/wav\"\n      ) {\n        return (\n          <PreviewAudioUri\n            src={info.value}\n            contentType={info.format.contentType}\n          />\n        );\n      } else {\n        return <PreviewUri value={info.value} type={info} />;\n      }\n    case \"datetime\":\n      if (info.format.parts === \"date\" || info.format.parts === \"datetime\") {\n        return <PreviewDate value={info.value} format={info.format} />;\n      }\n      return <></>;\n    case \"color\":\n      return <PreviewColor value={info.value} format={info.format} />;\n    case \"json\":\n      return <PreviewJson value={info.value} format={info.format} />;\n    default:\n      return <></>;\n  }\n}\n\nfunction PreviewJson({\n  value,\n  format,\n}: {\n  value: string;\n  format: JSONJSONFormat;\n}) {\n  if (format.variant === \"json5\") {\n    return <></>;\n  }\n\n  return <CodeViewer code={JSON.stringify(JSON.parse(value), null, 2)} />;\n}\n\nfunction PreviewColor({\n  value,\n  format,\n}: {\n  value: string;\n  format: JSONColorFormat;\n}) {\n  const color = new Color(value);\n\n  const textColor = color.isLight() ? \"text-slate-800\" : \"text-slate-100\";\n\n  return (\n    <>\n      <PreviewBox>\n        <div>\n          <div\n            className=\"flex items-center justify-center w-full h-52\"\n            style={{ backgroundColor: color.hex().toString() }}\n          >\n            <p className={`text-center text-xl ${textColor}`}>{value}</p>\n          </div>\n        </div>\n      </PreviewBox>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewUri.tsx",
    "content": "import { JSONStringType } from \"@jsonhero/json-infer-types/lib/@types\";\nimport { useEffect } from \"react\";\nimport { useFetcher } from \"remix\";\nimport { Body } from \"~/components/Primitives/Body\";\nimport { useLoadWhenOnline } from \"~/hooks/useLoadWhenOnline\";\nimport { PreviewBox } from \"../PreviewBox\";\nimport { PreviewResult } from \"./preview.types\";\nimport { PreviewUriElement } from \"./PreviewUriElement\";\n\nexport type PreviewUriProps = {\n  value: string;\n  type: JSONStringType;\n};\n\nexport function PreviewUri(props: PreviewUriProps) {\n  const previewFetcher = useFetcher<PreviewResult>();\n  const encodedUri = encodeURIComponent(props.value);\n  const load = () => previewFetcher.load(`/actions/getPreview/${encodedUri}`);\n\n  useLoadWhenOnline(load, [encodedUri]);\n\n  return (\n    <div>\n      {previewFetcher.type === \"done\" ? (\n        <>\n          {typeof previewFetcher.data == \"string\" ? (\n            <PreviewBox>\n              <Body>\n                <span\n                  dangerouslySetInnerHTML={{ __html: previewFetcher.data }}\n                ></span>\n              </Body>\n            </PreviewBox>\n          ) : \"error\" in previewFetcher.data ? (\n            <PreviewBox>\n              <Body>{previewFetcher.data.error}</Body>\n            </PreviewBox>\n          ) : (\n            <PreviewUriElement info={previewFetcher.data} />\n          )}\n        </>\n      ) : (\n        <PreviewBox>\n          <Body className=\"h-96 animate-pulse bg-slate-300 dark:text-slate-300 dark:bg-slate-500 flex justify-center items-center\">\n            Loading…\n          </Body>\n        </PreviewBox>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewUriElement.tsx",
    "content": "import { PreviewInfo } from \"./preview.types\";\nimport { PreviewHtml } from \"./PreviewHtml\";\nimport { PreviewImage } from \"./PreviewImage\";\nimport { PreviewJson } from \"./PreviewJson\";\n\nexport type PreviewUriElementProps = {\n  info: PreviewInfo;\n};\n\nexport function PreviewUriElement({ info }: PreviewUriElementProps) {\n  switch (info.contentType) {\n    case \"html\":\n      return <PreviewHtml info={info} />;\n    case \"image\":\n    case \"gif\":\n      return <PreviewImage info={info} />;\n    case \"json\":\n      return <PreviewJson preview={info} />;\n    default:\n      return <></>;\n  }\n}\n"
  },
  {
    "path": "app/components/Preview/Types/PreviewVideoUri.tsx",
    "content": "import { useRef } from \"react\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\nimport { Body } from \"~/components/Primitives/Body\";\nimport { PreviewBox } from \"../PreviewBox\";\n\nexport function PreviewVideoUri({\n  src,\n  contentType,\n}: {\n  src: string;\n  contentType: string;\n}) {\n  const videoRef = useRef<HTMLVideoElement>(null);\n\n  useHotkeys(\n    \"space\",\n    (e) => {\n      e.preventDefault();\n\n      if (videoRef.current) {\n        if (videoRef.current.paused) {\n          videoRef.current.play();\n        } else {\n          videoRef.current.pause();\n        }\n      }\n    },\n    [videoRef]\n  );\n\n  return (\n    <div>\n      <PreviewBox>\n        <Body>\n          <video key={src} controls ref={videoRef}>\n            <source src={src} type={contentType} />\n            Sorry, your browser doesn't support embedded videos.\n          </video>\n        </Body>\n      </PreviewBox>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/RetweetIcon.tsx",
    "content": "export function RetweetIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className={props.className}\n      viewBox=\"0 0 24 24\"\n      aria-hidden=\"true\"\n      stroke=\"currentColor\"\n    >\n      <g>\n        <path d=\"M23.77 15.67c-.292-.293-.767-.293-1.06 0l-2.22 2.22V7.65c0-2.068-1.683-3.75-3.75-3.75h-5.85c-.414 0-.75.336-.75.75s.336.75.75.75h5.85c1.24 0 2.25 1.01 2.25 2.25v10.24l-2.22-2.22c-.293-.293-.768-.293-1.06 0s-.294.768 0 1.06l3.5 3.5c.145.147.337.22.53.22s.383-.072.53-.22l3.5-3.5c.294-.292.294-.767 0-1.06zm-10.66 3.28H7.26c-1.24 0-2.25-1.01-2.25-2.25V6.46l2.22 2.22c.148.147.34.22.532.22s.384-.073.53-.22c.293-.293.293-.768 0-1.06l-3.5-3.5c-.293-.294-.768-.294-1.06 0l-3.5 3.5c-.294.292-.294.767 0 1.06s.767.293 1.06 0l2.22-2.22V16.7c0 2.068 1.683 3.75 3.75 3.75h5.85c.414 0 .75-.336.75-.75s-.337-.75-.75-.75z\"></path>\n      </g>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "app/components/Preview/Types/preview.types.d.ts",
    "content": "export declare type PreviewImage = {\n  url: string;\n  contentType: \"image\" | \"gif\";\n  mimeType: string;\n  size?: number;\n  image?: ImageAssetDetails;\n};\n\nexport declare type PreviewVideo = {\n  url: string;\n  contentType: \"video\";\n  mimeType: string;\n  size?: number;\n  image?: ImageAssetDetails;\n};\n\nexport declare type PreviewHtml = {\n  url: string;\n  contentType: \"html\";\n  mimeType: string;\n  title?: string;\n  description?: string;\n  name?: string;\n  icon?: ImageAssetDetails;\n  image?: ImageAssetDetails;\n  details?: YouTubeLinkDetails | TwitterLinkDetails;\n};\n\nexport declare type PreviewJson = {\n  url: string;\n  contentType: \"json\";\n  json: unknown;\n};\n\nexport declare type PreviewInfo = PreviewImage | PreviewHtml | PreviewJson;\nexport declare type PreviewResult = PreviewInfo | { error: string };\n\ndeclare type YouTubeLinkDetails = {\n  type: \"youtube\";\n  videoId: string;\n  duration: string;\n  viewCount: number;\n  likeCount: number;\n  dislikeCount: number;\n  commentCount: number;\n  publishedAt: string;\n};\n\ndeclare type TwitterLinkDetails = {\n  type: \"twitter\";\n  statusId: string;\n  retweetCount: number;\n  likesCount: number;\n  publishedAt: string;\n};\n\ndeclare type ImageAssetDetails = {\n  url: string;\n  alt?: string;\n  width?: number;\n  height?: number;\n};\n\n/* OpenGraph Ninja Types */\n// Types adapted from https://github.com/opengraphninja/react/blob/main/src/types.d.ts\ntype OpenGraphMedia = {\n  height: string | null;\n  type: string | null;\n  url: string;\n  width: string | null;\n};\n\ntype OpenGraphTwitterImage = {\n  height: string | null;\n  alt: string | null;\n  url: string;\n  width: string | null;\n};\n\ntype OpenGraphTwitterPlayer = {\n  height: string | null;\n  stream: string | null;\n  url: string;\n  width: string | null;\n};\n\ntype OpenGraphMusicSong = {\n  url: string;\n  track: string | null;\n  disc: string | null;\n};\n\ntype OpenGraphDetails = {\n  alAndroidAppName?: string;\n  alAndroidClass?: string;\n  alAndroidPackage?: string;\n  alAndroidUrl?: string;\n  alIosAppName?: string;\n  alIosAppStoreId?: string;\n  alIosUrl?: string;\n  alIpadAppName?: string;\n  alIpadAppStoreId?: string;\n  alIpadUrl?: string;\n  alIphoneAppName?: string;\n  alIphoneAppStoreId?: string;\n  alIphoneUrl?: string;\n  alWebShouldFallback?: string;\n  alWebUrl?: string;\n  alWindowsAppId?: string;\n  alWindowsAppName?: string;\n  alWindowsPhoneAppId?: string;\n  alWindowsPhoneAppName?: string;\n  alWindowsPhoneUrl?: string;\n  alWindowsUniversalAppId?: string;\n  alWindowsUniversalAppName?: string;\n  alWindowsUniversalUrl?: string;\n  alWindowsUrl?: string;\n  articleAuthor?: string;\n  articleExpirationTime?: string;\n  articleModifiedTime?: string;\n  articlePublishedTime?: string;\n  articlePublisher?: string;\n  articleSection?: string;\n  articleTag?: string;\n  author?: string;\n  bookAuthor?: string;\n  bookCanonicalName?: string;\n  bookIsbn?: string;\n  bookReleaseDate?: string;\n  booksBook?: string;\n  booksRatingScale?: string;\n  booksRatingValue?: string;\n  bookTag?: string;\n  businessContactDataCountryName?: string;\n  businessContactDataLocality?: string;\n  businessContactDataPostalCode?: string;\n  businessContactDataRegion?: string;\n  businessContactDataStreetAddress?: string;\n  dcContributor?: string;\n  dcCoverage?: string;\n  dcCreator?: string;\n  dcDate?: string;\n  dcDateCreated?: string;\n  dcDateIssued?: string;\n  dcDescription?: string;\n  dcFormatMedia?: string;\n  dcFormatSize?: string;\n  dcIdentifier?: string;\n  dcLanguage?: string;\n  dcPublisher?: string;\n  dcRelation?: string;\n  dcRights?: string;\n  dcSource?: string;\n  dcSubject?: string;\n  dcTitle?: string;\n  dcType?: string;\n  modifiedTime?: string;\n  musicAlbum?: string | string[];\n  musicAlbumDisc?: string;\n  musicAlbumTrack?: string;\n  musicAlbumUrl?: string;\n  musicCreator?: string | string[];\n  musicDuration?: string;\n  musicMusician?: string | string[];\n  musicReleaseDate?: string;\n  musicSong?: OpenGraphMusicSong;\n  musicSongDisc?: string | string[];\n  musicSongTrack?: string | string[];\n  musicSongUrl?: string | string[];\n  ogArticleAuthor?: string;\n  ogArticleExpirationTime?: string;\n  ogArticleModifiedTime?: string;\n  ogArticlePublishedTime?: string;\n  ogArticlePublisher?: string;\n  ogArticleSection?: string;\n  ogArticleTag?: string;\n  ogAudio?: string;\n  ogAudioSecureURL?: string;\n  ogAudioType?: string;\n  ogAudioURL?: string;\n  ogAvailability?: string;\n  ogDate?: string;\n  ogDescription?: string;\n  ogDeterminer?: string;\n  ogImage?: OpenGraphMedia | OpenGraphMedia[];\n  ogImageHeight?: string | string[];\n  ogImageSecureURL?: string | string[];\n  ogImageType?: string | string[];\n  ogImageURL?: string | string[];\n  ogImageWidth?: string | string[];\n  ogLocale?: string;\n  ogLocaleAlternate?: string;\n  ogLogo?: string;\n  ogPriceAmount?: string;\n  ogPriceCurrency?: string;\n  ogProductAvailability?: string;\n  ogProductCondition?: string;\n  ogProductPriceAmount?: string;\n  ogProductPriceCurrency?: string;\n  ogProductRetailerItemId?: string;\n  ogSiteName?: string;\n  ogTitle?: string;\n  ogType?: string;\n  ogUrl?: string;\n  ogVideo?: OpenGraphMedia | OpenGraphMedia[];\n  ogVideoActorId?: string | string[];\n  ogVideoHeight?: string | string[];\n  ogVideoSecureURL?: string | string[];\n  ogVideoType?: string | string[];\n  ogVideoWidth?: string | string[];\n  placeLocationLatitude?: string;\n  placeLocationLongitude?: string;\n  profileFirstName?: string;\n  profileGender?: string;\n  profileLastName?: string;\n  profileUsername?: string;\n  publishedTime?: string;\n  releaseDate?: string;\n  restaurantContactInfoCountryName?: string;\n  restaurantContactInfoEmail?: string;\n  restaurantContactInfoLocality?: string;\n  restaurantContactInfoPhoneNumber?: string;\n  restaurantContactInfoPostalCode?: string;\n  restaurantContactInfoRegion?: string;\n  restaurantContactInfoStreetAddress?: string;\n  restaurantContactInfoWebsite?: string;\n  restaurantMenu?: string;\n  restaurantRestaurant?: string;\n  restaurantSection?: string;\n  restaurantVariationPriceAmount?: string;\n  restaurantVariationPriceCurrency?: string;\n  twitterAppIdGooglePlay?: string;\n  twitterAppIdiPad?: string;\n  twitterAppIdiPhone?: string;\n  twitterAppNameGooglePlay?: string;\n  twitterAppNameiPad?: string;\n  twitterAppNameiPhone?: string;\n  twitterAppUrlGooglePlay?: string;\n  twitterAppUrliPad?: string;\n  twitterAppUrliPhone?: string;\n  twitterCard?: string;\n  twitterCreator?: string;\n  twitterCreatorId?: string;\n  twitterDescription?: string;\n  twitterImage?: OpenGraphTwitterImage | OpenGraphTwitterImage[];\n  twitterImageAlt?: string | string[];\n  twitterImageHeight?: string | string[];\n  twitterImageSrc?: string | string[];\n  twitterImageWidth?: string | string[];\n  twitterPlayer?: OpenGraphTwitterPlayer | OpenGraphTwitterPlayer[];\n  twitterPlayerHeight?: string | string[];\n  twitterPlayerStream?: string | string[];\n  twitterPlayerStreamContentType?: string | string[];\n  twitterPlayerWidth?: string | string[];\n  twitterSite?: string;\n  twitterSiteId?: string;\n  twitterTitle?: string;\n  twitterUrl?: string;\n  updatedTime?: string;\n  favicon?: string;\n  [key: string]: any;\n};\n\nexport type OpenGraphPreviewData = {\n  hostname: string;\n  requestUrl: string;\n  title: string;\n  description: string;\n  image?: {\n    url: string;\n    alt?: string;\n  };\n  details: Details;\n};\n\nexport type OpenGraphPreviewDataError = {\n  error: string;\n};\n"
  },
  {
    "path": "app/components/Primitives/Body.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const Body: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return <p className={`font-sans text-base ${className}`}>{children}</p>;\n};\n"
  },
  {
    "path": "app/components/Primitives/BodyBold.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const BodyBold: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return (\n    <p className={`font-sans text-base font-bold ${className}`}>{children}</p>\n  );\n};\n"
  },
  {
    "path": "app/components/Primitives/ExtraLargeTitle.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const ExtraLargeTitle: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return (\n    <h1 className={`font-sans font-bold text-6xl ${className}`}>{children}</h1>\n  );\n};\n"
  },
  {
    "path": "app/components/Primitives/LargeMono.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const LargeMono: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return <p className={`font-mono text-md ${className}`}>{children}</p>;\n};\n"
  },
  {
    "path": "app/components/Primitives/LargeTitle.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const LargeTitle: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return (\n    <h1 className={`font-sans font-bold text-2xl ${className}`}>{children}</h1>\n  );\n};\n"
  },
  {
    "path": "app/components/Primitives/Mono.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const Mono: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return <p className={`font-mono text-sm ${className}`}>{children}</p>;\n};\n"
  },
  {
    "path": "app/components/Primitives/PageNotFoundTitle.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const PageNotFoundTitle: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return (\n    <h1 className={`font-sans font-bold text-8xl ${className}`}>{children}</h1>\n  );\n};\n"
  },
  {
    "path": "app/components/Primitives/SmallBody.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const SmallBody: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return <p className={`font-sans text-sm ${className}`}>{children}</p>;\n};\n"
  },
  {
    "path": "app/components/Primitives/SmallSubtitle.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const SmallSubtitle: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return (\n    <h3 className={`font-sans text-xl text-slate-300 ${className}`}>{children}</h3>\n  );\n};\n"
  },
  {
    "path": "app/components/Primitives/SmallTitle.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const SmallTitle: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return (\n    <h3 className={`font-sans font-bold text-lg ${className}`}>{children}</h3>\n  );\n};\n"
  },
  {
    "path": "app/components/Primitives/Title.tsx",
    "content": "import { FunctionComponent } from \"react\";\n\nexport const Title: FunctionComponent<{ className?: string }> = ({\n  className,\n  children,\n}) => {\n  return (\n    <h2 className={`font-sans font-bold text-xl ${className}`}>{children}</h2>\n  );\n};\n"
  },
  {
    "path": "app/components/Properties/PropertiesFloat.tsx",
    "content": "import { JSONFloatType } from \"@jsonhero/json-infer-types\";\nimport { formatValue } from \"~/utilities/formatter\";\nimport { DataTable } from \"../DataTable\";\nimport { ValueIcon } from \"../ValueIcon\";\n\nexport type PropertiesFloatProps = {\n  type: JSONFloatType;\n};\n\nexport function PropertiesFloat(info: PropertiesFloatProps) {\n  return (\n    <DataTable\n      rows={[\n        {\n          key: \"Formatted value\",\n          value: formatValue(info.type) ?? \"\",\n          icon: <ValueIcon type={info.type} />,\n        },\n        {\n          key: \"Type\",\n          value: info.type.name,\n        },\n      ]}\n    />\n  );\n}"
  },
  {
    "path": "app/components/Properties/PropertiesInt.tsx",
    "content": "import { JSONIntType } from \"@jsonhero/json-infer-types\";\nimport {\n  JSONTimestampFormat,\n} from \"@jsonhero/json-infer-types/lib/formats\";\nimport { formatValue } from \"~/utilities/formatter\";\nimport { DataTable } from \"../DataTable\";\nimport { ValueIcon } from \"../ValueIcon\";\n\nexport type PropertiesNumberProps = {\n  type: JSONIntType;\n};\n\nexport function PropertiesInt({ type }: { type: JSONIntType }) {\n  if (type.format == null) {\n    return (\n      <DataTable\n        rows={[\n          {\n            key: \"Formatted value\",\n            value: formatValue(type) ?? \"\",\n            icon: <ValueIcon type={type} />,\n          },\n          {\n            key: \"Type\",\n            value: type.name,\n          },\n        ]}\n      />\n    );\n  }\n  switch (type.format.name) {\n    case \"timestamp\":\n      return <PropertiesTimestamp value={type.value} format={type.format} />;\n    default:\n      return <></>;\n  }\n}\n\nfunction PropertiesTimestamp({\n  value,\n  format,\n}: {\n  value: number;\n  format: JSONTimestampFormat;\n}) {\n  const date =\n    format.variant === \"millisecondsSinceEpoch\"\n      ? new Date(value)\n      : format.variant === \"secondsSinceEpoch\"\n      ? new Date(value * 1000)\n      : new Date(value / 1000000);\n\n  const properties = [\n    {\n      key: \"rfc3339\",\n      value: date.toISOString(),\n    },\n    {\n      key: \"rfc2822\",\n      value: date.toUTCString(),\n    },\n    {\n      key: \"unix\",\n      value: (date.getTime() / 1000).toFixed(0),\n    },\n    {\n      key: \"unix ms\",\n      value: date.getTime().toString(),\n    },\n    {\n      key: \"date\",\n      value: date.toDateString(),\n    },\n    {\n      key: \"time\",\n      value: date.toTimeString(),\n    },\n  ];\n\n  return <DataTable rows={properties} />;\n}"
  },
  {
    "path": "app/components/Properties/PropertiesString.tsx",
    "content": "import { JSONStringType, JSONURIFormat } from \"@jsonhero/json-infer-types\";\nimport {\n  JSONColorFormat,\n  JSONDateTimeFormat,\n  JSONJWTStringFormat,\n  JSONTimestampFormat,\n} from \"@jsonhero/json-infer-types/lib/formats\";\nimport Color from \"color\";\nimport { DataTableRow, DataTable } from \"../DataTable\";\n\nexport type PropertiesStringProps = {\n  type: JSONStringType;\n};\n\nexport function PropertiesString({ type }: { type: JSONStringType }) {\n  if (type.format == null) {\n    return <></>;\n  }\n\n  switch (type.format.name) {\n    case \"uri\":\n      return <PropertiesUri value={type.value} format={type.format} />;\n    case \"color\":\n      return <PropertiesColor value={type.value} format={type.format} />;\n    case \"datetime\":\n      return <PropertiesDateTime value={type.value} format={type.format} />;\n    case \"timestamp\":\n      return <PropertiesTimestamp value={type.value} format={type.format} />;\n    case \"jwt\":\n      return <PropertiesJwt value={type.value} format={type.format} />;\n    default:\n      return <></>;\n  }\n}\n\nimport jwtDecode from \"jwt-decode\";\nimport { inferTemporal } from \"~/utilities/inferredTemporal\";\n\nfunction PropertiesJwt({\n  value,\n  format,\n}: {\n  value: string;\n  format: JSONJWTStringFormat;\n}) {\n  const properties: DataTableRow[] = [];\n\n  const decodedPayload = jwtDecode(value) as Record<string, any>;\n\n  for (const [key, value] of Object.entries(decodedPayload)) {\n    properties.push({\n      key: key,\n      value,\n    });\n  }\n\n  const decodedHeader = jwtDecode(value, { header: true }) as Record<\n    string,\n    any\n  >;\n\n  for (const [key, value] of Object.entries(decodedHeader)) {\n    properties.push({\n      key: key,\n      value,\n    });\n  }\n\n  return <DataTable rows={properties} />;\n}\n\nfunction PropertiesTimestamp({\n  value,\n  format,\n}: {\n  value: string;\n  format: JSONTimestampFormat;\n}) {\n  const date =\n    format.variant === \"millisecondsSinceEpoch\"\n      ? new Date(parseInt(value))\n      : format.variant === \"secondsSinceEpoch\"\n      ? new Date(parseInt(value) * 1000)\n      : new Date(parseInt(value) / 1000000);\n\n  const properties = [\n    {\n      key: \"rfc3339\",\n      value: date.toISOString(),\n    },\n    {\n      key: \"rfc2822\",\n      value: date.toUTCString(),\n    },\n    {\n      key: \"unix\",\n      value: (date.getTime() / 1000).toFixed(0),\n    },\n    {\n      key: \"unix ms\",\n      value: date.getTime().toString(),\n    },\n    {\n      key: \"date\",\n      value: date.toDateString(),\n    },\n    {\n      key: \"time\",\n      value: date.toTimeString(),\n    },\n  ];\n\n  return <DataTable rows={properties} />;\n}\n\nfunction PropertiesDateTime({\n  value,\n  format,\n}: {\n  value: string;\n  format: JSONDateTimeFormat;\n}) {\n  if (format.parts === \"time\") {\n    return <></>;\n  }\n\n  const temporal = inferTemporal(value, format);\n\n  if (!temporal) {\n    return <></>;\n  }\n\n  const properties = [\n    {\n      key: \"rfc3339\",\n      value: temporal.toString(),\n    },\n    // {\n    //   key: \"unix\",\n    //   value: (date.getTime() / 1000).toFixed(0),\n    // },\n    // {\n    //   key: \"unix ms\",\n    //   value: date.getTime().toString(),\n    // },\n    // {\n    //   key: \"date\",\n    //   value: date.toDateString(),\n    // },\n    // {\n    //   key: \"time\",\n    //   value: date.toTimeString(),\n    // },\n  ];\n\n  if (\"epochSeconds\" in temporal) {\n    properties.push({\n      key: \"unix\",\n      value: temporal.epochSeconds.toString(),\n    });\n  }\n\n  if (\"epochMilliseconds\" in temporal) {\n    properties.push({\n      key: \"unix ms\",\n      value: temporal.epochMilliseconds.toString(),\n    });\n  }\n\n  properties.push({\n    key: \"date\",\n    value: temporal.toLocaleString(\"en-US\", {\n      year: \"numeric\",\n      month: \"long\",\n      day: \"numeric\",\n    }),\n  });\n\n  return <DataTable rows={properties} />;\n}\n\nfunction PropertiesColor({\n  value,\n  format,\n}: {\n  value: string;\n  format: JSONColorFormat;\n}) {\n  const color = new Color(value);\n\n  const properties = [\n    {\n      key: \"hex\",\n      value: color.hex(),\n    },\n    {\n      key: \"rgb\",\n      value: color.rgb().string(),\n    },\n    {\n      key: \"hsl\",\n      value: color.hsl().string(),\n    },\n    {\n      key: \"luminosity\",\n      value: color.luminosity().toFixed(4),\n    },\n    {\n      key: \"contrastRatio\",\n      value: color.isLight() ? \"light\" : \"dark\",\n    },\n  ];\n\n  return <DataTable rows={properties} />;\n}\n\nfunction PropertiesUri({\n  value,\n  format,\n}: {\n  value: string;\n  format: JSONURIFormat;\n}) {\n  let uri = new URL(value);\n\n  let standardProperties: DataTableRow[] = [\n    {\n      key: \"href\",\n      value: uri.href,\n    },\n    {\n      key: \"origin\",\n      value: uri.origin,\n    },\n    {\n      key: \"protocol\",\n      value: uri.protocol,\n    },\n    {\n      key: \"hostname\",\n      value: uri.hostname,\n    },\n    {\n      key: \"pathname\",\n      value: uri.pathname,\n    },\n  ];\n\n  if (uri.search) {\n    standardProperties.push({\n      key: \"search\",\n      value: uri.search,\n    });\n  }\n\n  if (uri.hash) {\n    standardProperties.push({\n      key: \"hash\",\n      value: uri.hash,\n    });\n  }\n\n  if (format.contentType != null) {\n    standardProperties.push({\n      key: \"mimeType\",\n      value: format.contentType,\n    });\n  }\n\n  return <DataTable rows={standardProperties} />;\n}\n"
  },
  {
    "path": "app/components/Properties/PropertiesValue.tsx",
    "content": "import { useSelectedInfo } from \"~/hooks/useSelectedInfo\";\nimport { PropertiesInt } from \"./PropertiesInt\";\nimport { PropertiesFloat } from \"./PropertiesFloat\";\nimport { PropertiesString } from \"./PropertiesString\";\n\nexport function PropertiesValue() {\n  const info = useSelectedInfo();\n\n  if (!info) {\n    return <></>;\n  }\n\n  switch (info.name) {\n    case \"float\":\n      return <PropertiesFloat type={info} />;\n    case \"int\":\n      return <PropertiesInt type={info} />;\n    case \"string\":\n      return <PropertiesString type={info} />;\n    default:\n      return <></>;\n  }\n}\n"
  },
  {
    "path": "app/components/RelatedValues.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport { Title } from \"~/components/Primitives/Title\";\nimport { useJson } from \"../hooks/useJson\";\nimport { Mono } from \"./Primitives/Mono\";\nimport { SmallBody } from \"./Primitives/SmallBody\";\nimport { generateNodesToPath } from \"~/utilities/jsonColumnView\";\nimport { useJsonColumnViewState } from \"../hooks/useJsonColumnView\";\nimport {\n  RelatedValuesGroup,\n  groupRelatedValues,\n} from \"~/utilities/relatedValues\";\nimport { PathPreview } from \"./PathPreview\";\nimport { ChevronDownIcon, ChevronRightIcon } from \"@heroicons/react/outline\";\n\nexport type RelatedValuesProps = {\n  relatedPaths: string[];\n};\n\nexport function RelatedValues({ relatedPaths }: RelatedValuesProps) {\n  const [json] = useJson();\n  const { selectedNodeId } = useJsonColumnViewState();\n  const [openId, setOpenId] = useState<string | null>(null);\n\n  const relatedValuesGroups = useMemo<Array<RelatedValuesGroup>>(() => {\n    if (!selectedNodeId) {\n      return [];\n    }\n    return groupRelatedValues(relatedPaths, json);\n  }, [json, relatedPaths]);\n\n  const toggleOpen = (id: string) => {\n    if (openId === id) {\n      setOpenId(null);\n    } else {\n      setOpenId(id);\n    }\n  };\n\n  return (\n    <>\n      {relatedValuesGroups.length > 0 && (\n        <div className=\"my-4\">\n          <Title className=\"mb-2 text-slate-700 transition dark:text-slate-400\">\n            Related values\n          </Title>\n          {relatedValuesGroups.map((relatedValuesGroup, i) => {\n            return (\n              <RelatedValuesGroupItem\n                group={relatedValuesGroup}\n                key={relatedValuesGroup.value}\n                isOpen={relatedValuesGroup.value === openId}\n                toggleOpen={() => toggleOpen(relatedValuesGroup.value)}\n              />\n            );\n          })}\n        </div>\n      )}\n    </>\n  );\n}\n\nfunction RelatedValuesGroupItem({\n  group,\n  isOpen,\n  toggleOpen,\n}: {\n  group: RelatedValuesGroup;\n  isOpen: boolean;\n  toggleOpen: () => void;\n}) {\n  const isLinkable = group.value !== \"undefined\";\n  const isHighlighted = group.value === \"undefined\" || group.value === \"null\";\n\n  return (\n    <div className=\"mb-1 transition dark:text-slate-300\">\n      <div\n        className={`flex rounded-sm transition hover:cursor-pointer ${\n          isOpen\n            ? \"bg-slate-200 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-700\"\n            : \"bg-slate-100 hover:bg-slate-200 dark:bg-slate-600 dark:hover:bg-slate-700\"\n        }`}\n        onClick={() => toggleOpen()}\n      >\n        <div className=\"flex items-center rounded-sm px-1 bg-slate-200 dark:bg-slate-700\">\n          {isOpen ? (\n            <ChevronDownIcon className=\"w-3 h-3\" />\n          ) : (\n            <ChevronRightIcon className=\"w-3 h-3\" />\n          )}\n          <SmallBody className=\"ml-1\">{group.paths.length}</SmallBody>\n        </div>\n        <Mono\n          className={`truncate px-2 text-slate-700 dark:text-slate-200 ${\n            isHighlighted ? \"italic\" : \"\"\n          }`}\n        >\n          {group.value}\n        </Mono>\n      </div>\n      {isOpen &&\n        group.paths.map((path) => {\n          return (\n            <div\n              className=\"p-0.5 bg-slate-100 dark:bg-slate-700 dark:bg-opacity-60\"\n              key={path}\n            >\n              <PathLink path={path} enabled={isLinkable} />\n            </div>\n          );\n        })}\n    </div>\n  );\n}\n\nfunction PathLink({ path, enabled }: { path: string; enabled: boolean }) {\n  const [json] = useJson();\n\n  const selectedNodes = useMemo(() => {\n    return generateNodesToPath(json, path);\n  }, [json, path]);\n\n  return (\n    <PathPreview nodes={selectedNodes} maxComponents={4} enabled={enabled} />\n  );\n}\n"
  },
  {
    "path": "app/components/Resizable.tsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\n\ntype ResizableProps = {\n  children: React.ReactNode;\n  isHorizontal: boolean;\n  initialSize: number;\n  minimumSize: number;\n  maximumSize: number;\n};\n\nexport default function Resizable({\n  children,\n  isHorizontal = true,\n  initialSize,\n  minimumSize,\n  maximumSize,\n}: ResizableProps) {\n  const [dimension, setDimension] = useState(initialSize);\n  const previousDragPosition = useRef<{ x: number; y: number } | null>(null);\n\n  const handleDragStart = (e: React.MouseEvent<HTMLDivElement>): void => {\n    previousDragPosition.current = {\n      x: e.clientX,\n      y: e.clientY,\n    };\n  };\n\n  const handleDrag = (e: MouseEvent) => {\n    if (previousDragPosition.current == null) {\n      return;\n    }\n\n    e.preventDefault();\n\n    let offset = 0;\n    if (isHorizontal) {\n      offset = e.clientX - previousDragPosition.current.x;\n    } else {\n      offset = e.clientY - previousDragPosition.current.y;\n    }\n    let newValue = dimension - offset;\n    if (minimumSize != null) {\n      newValue = Math.max(minimumSize, newValue);\n    }\n    if (maximumSize != null) {\n      newValue = Math.min(maximumSize, newValue);\n    }\n    setDimension(newValue);\n    previousDragPosition.current = {\n      x: e.clientX,\n      y: e.clientY,\n    };\n  };\n\n  const handleDragEnd = () => {\n    previousDragPosition.current = null;\n  };\n\n  useEffect(() => {\n    window.addEventListener(\"mousemove\", handleDrag);\n    window.addEventListener(\"mouseup\", handleDragEnd);\n    return () => {\n      window.removeEventListener(\"mousemove\", handleDrag);\n      window.removeEventListener(\"mouseup\", handleDragEnd);\n    };\n  }, [handleDrag, handleDragEnd]);\n\n  const style = () => {\n    let formatted = dimension + \"px\";\n\n    if (isHorizontal) {\n      return {\n        width: formatted,\n      };\n    } else {\n      return {\n        height: formatted,\n      };\n    }\n  };\n\n  const classes = () => {\n    if (isHorizontal) {\n      return \"flex flex-none relative\";\n    } else {\n      return \"flex flex-none relative\";\n    }\n  };\n\n  return (\n    <div style={style()} className={classes()}>\n      <div className={\"flex-grow\"} style={{ width: \"inherit\" }}>\n        {children}\n      </div>\n      <div\n        className={\n          isHorizontal\n            ? \"w-1 h-full absolute my-0 -ml-[1px] transition-all cursor-col-resize hover:bg-indigo-700 hover:opacity-100\"\n            : \"h-1 w-full transition-all cursor-row-resize hover:bg-indigo-700 hover:opacity-100\"\n        }\n        onMouseDown={handleDragStart}\n      ></div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/SampleUrls.tsx",
    "content": "import { ExampleDoc } from \"./ExampleDoc\";\n\nexport function SampleUrls() {\n  return (\n    <div className=\"flex justify-start flex-wrap gap-2\">\n      <ExampleDoc\n        id=\"d9udW60cLOok\"\n        title=\"Tweet JSON\"\n        path=\"data.0.entities.urls.0.expanded_url\"\n      />\n      <ExampleDoc id=\"PjHo1o5MVeH4\" title=\"Github API\" />\n      <ExampleDoc\n        id=\"XKqIsPgCssUN\"\n        title=\"Airtable API\"\n        path=\"records.3.createdTime\"\n      />\n      <ExampleDoc\n        id=\"bSc7r1Ta0fED\"\n        title=\"Unsplash API\"\n        path=\"4.urls.regular\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/SearchBar.tsx",
    "content": "import { SearchIcon } from \"@heroicons/react/outline\";\nimport { ShortcutIcon } from \"./Icons/ShortcutIcon\";\nimport { Body } from \"./Primitives/Body\";\nimport { Dialog, DialogTrigger, DialogContent } from \"./UI/Dialog\";\n\nimport classnames from \"~/utilities/classnames\";\nimport { SearchPalette } from \"./SearchPalette\";\nimport { useState } from \"react\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\nimport { useJsonColumnViewAPI } from \"~/hooks/useJsonColumnView\";\nimport { useJsonSearchApi } from \"~/hooks/useJsonSearch\";\n\nexport function SearchBar() {\n  const [isOpen, setIsOpen] = useState(false);\n  const { goToNodeId } = useJsonColumnViewAPI();\n  const searchApi = useJsonSearchApi();\n\n  useHotkeys(\n    \"cmd+k,ctrl+k\",\n    (e) => {\n      e.preventDefault();\n      setIsOpen(true);\n    },\n    [setIsOpen]\n  );\n\n  return (\n    <Dialog open={isOpen} onOpenChange={() => !isOpen && searchApi.reset()}>\n      <DialogTrigger\n        className=\"focus:outline-none focus-visible:outline-none\"\n        onClick={() => setIsOpen(true)}\n      >\n        <div className=\"flex justify-between items-center group w-44 py-[3px] rounded bg-slate-300 transition hover:bg-slate-400 hover:bg-opacity-50 dark:bg-slate-800 dark:text-slate-400 hover:cursor-pointer hover:dark:bg-slate-700 hover:dark:bg-opacity-70\">\n          <div className=\"flex items-center pl-1\">\n            <SearchIcon className=\"w-4 h-4 mr-1\" />\n            <Body>Search…</Body>\n          </div>\n          <div className=\"flex items-center gap-1 pr-1\">\n            <ShortcutIcon className=\"w-4 h-4 text-sm bg-slate-200 transition group-hover:bg-slate-100 dark:bg-slate-700 dark:group-hover:bg-slate-600\">\n              ⌘\n            </ShortcutIcon>\n            <ShortcutIcon className=\"w-4 h-4 text-sm bg-slate-200 transition group-hover:bg-slate-100 dark:bg-slate-700 dark:group-hover:bg-slate-600\">\n              K\n            </ShortcutIcon>\n          </div>\n        </div>\n      </DialogTrigger>\n      <DialogContent\n        onOverlayClick={() => setIsOpen(false)}\n        className={classnames(\n          \"fixed z-50\",\n          \"w-[95vw] max-w-2xl rounded-lg\",\n          \"top-0 left-[50%] -translate-x-[50%]\",\n          \"mt-[60px]\",\n          \"bg-white border-[1px] border-slate-500 dark:border-slate-700 dark:bg-slate-800\"\n        )}\n      >\n        <SearchPalette\n          onClose={() => setIsOpen(false)}\n          onSelect={(entry) => {\n            setIsOpen(false);\n            goToNodeId(entry, \"search\");\n          }}\n        />\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/components/SearchPalette.tsx",
    "content": "import { useJsonSearchApi, useJsonSearchState } from \"~/hooks/useJsonSearch\";\nimport {\n  ChevronRightIcon,\n  ExclamationIcon,\n  SearchIcon,\n} from \"@heroicons/react/outline\";\nimport { EscapeKeyIcon } from \"./Icons/EscapeKeyIcon\";\nimport { ArrowKeysUpDownIcon } from \"./Icons/ArrowKeysUpDownIcon\";\nimport { LoadingIcon } from \"./Icons/LoadingIcon\";\nimport { Body } from \"./Primitives/Body\";\nimport { ShortcutIcon } from \"./Icons/ShortcutIcon\";\nimport { Mono } from \"./Primitives/Mono\";\nimport {\n  useCombobox,\n  UseComboboxState,\n  UseComboboxStateChangeOptions,\n} from \"downshift\";\nimport { getComponentSlices, getStringSlices } from \"~/utilities/search\";\nimport classnames from \"~/utilities/classnames\";\nimport { iconForValue } from \"~/utilities/icons\";\nimport { useRef, useCallback } from \"react\";\nimport { useVirtual } from \"react-virtual\";\nimport { truncate } from \"lodash-es\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\nimport { useJson } from \"~/hooks/useJson\";\nimport { SearchResult } from \"@jsonhero/fuzzy-json-search\";\nimport { Match } from \"@jsonhero/fuzzy-json-search/lib/fuzzyScoring\";\n\nexport function SearchPalette({\n  onSelect,\n  onClose,\n}: {\n  onSelect?: (entry: string) => void;\n  onClose?: () => void;\n}) {\n  const searchState = useJsonSearchState();\n  const searchApi = useJsonSearchApi();\n\n  useHotkeys(\n    \"esc\",\n    (e) => {\n      e.preventDefault();\n      searchApi.reset();\n      onClose?.();\n    },\n    [onClose]\n  );\n\n  const listRef = useRef<HTMLElement>(null);\n\n  const rowVirtualizer = useVirtual({\n    size: (searchState.results ?? []).length,\n    parentRef: listRef,\n    estimateSize: useCallback(() => 70, []),\n    overscan: 6,\n  });\n\n  function comboboxReducer(\n    state: UseComboboxState<SearchResult<string>>,\n    actionAndChanges: UseComboboxStateChangeOptions<SearchResult<string>>\n  ): Partial<UseComboboxState<SearchResult<string>>> {\n    const { changes, ...action } = actionAndChanges;\n\n    // Don't update the input field when selecting an item\n    switch (action.type) {\n      case useCombobox.stateChangeTypes.ItemClick:\n      case useCombobox.stateChangeTypes.InputKeyDownEnter: {\n        return {\n          ...changes,\n          inputValue: state.inputValue,\n        };\n      }\n      default:\n        return changes;\n    }\n  }\n\n  const cb = useCombobox({\n    items: searchState.results ?? [],\n    stateReducer: comboboxReducer,\n    circularNavigation: false,\n    scrollIntoView: () => {},\n    onSelectedItemChange: ({ selectedItem }) => {\n      if (selectedItem) {\n        onSelect?.(selectedItem.item);\n        searchApi.reset();\n      }\n    },\n    onHighlightedIndexChange: ({ highlightedIndex }) =>\n      highlightedIndex && rowVirtualizer.scrollToIndex(highlightedIndex),\n    onInputValueChange: ({ inputValue }) =>\n      inputValue ? searchApi.search(inputValue) : searchApi.reset(),\n  });\n\n  const handleInputKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Escape\" && onClose && cb.inputValue.length === 0) {\n        searchApi.reset();\n        onClose?.();\n      }\n    },\n    [onClose, cb.inputValue]\n  );\n\n  return (\n    <>\n      <div\n        {...cb.getComboboxProps()}\n        className=\"max-h-[60vh] px-4 pt-4 overflow-hidden\"\n      >\n        <label\n          {...cb.getLabelProps()}\n          className=\"relative text-slate-400 focus-within:text-slate-600 block\"\n        >\n          <SearchIcon className=\"absolute w-7 h-7 top-1/2 transform -translate-y-1/2 left-3 text-slate-700 transition dark:text-white pointer-events-none\" />\n          <input\n            {...cb.getInputProps({ onKeyDown: handleInputKeyDown })}\n            type=\"text\"\n            spellCheck=\"false\"\n            placeholder=\"Search the JSON…\"\n            className=\"w-full pl-12 pr-4 py-4 rounded-sm text-slate-900 bg-slate-100 text-2xl caret-indigo-700 border-indigo-700 transition dark:text-white dark:bg-slate-900 focus:outline-none focus:ring focus:ring-indigo-700\"\n          />\n        </label>\n        <div className=\"flex flex-col mt-4 mb-2\">\n          <div className=\"results flex\">\n            {searchState.status !== \"idle\" &&\n              (!searchState.results || searchState.results.length === 0) && (\n                <div className=\"results-loading flex\">\n                  <LoadingIcon className=\"animate-spin h-5 w-5 mr-1\"></LoadingIcon>\n                  <Body className=\"text-slate-400\">Loading…</Body>\n                </div>\n              )}\n            {searchState.results && searchState.results.length > 0 && (\n              <div className=\"results-returned\">\n                <Body className=\"text-slate-400\">\n                  {searchState.results.length === 1\n                    ? \"1 result\"\n                    : `${searchState.results.length} results`}\n                </Body>\n              </div>\n            )}\n            {searchState.status === \"idle\" &&\n              searchState.query &&\n              searchState.query.length > 1 &&\n              (!searchState.results || searchState.results.length === 0) && (\n                <div className=\"results-none flex\">\n                  <ExclamationIcon className=\"h-5 w-5 mr-1 text-white\"></ExclamationIcon>\n                  <Body className=\"text-slate-400\">\n                    No results for \"{cb.inputValue}\"\n                  </Body>\n                </div>\n              )}\n          </div>\n        </div>\n        <ul\n          {...cb.getMenuProps({ ref: listRef })}\n          className=\"w-full max-h-[calc(60vh-120px)] overflow-y-auto relative\"\n        >\n          <li\n            key=\"total-size\"\n            style={{ height: rowVirtualizer.totalSize }}\n            className=\"mb-[1rem]\"\n          />\n          {rowVirtualizer.virtualItems.map((virtualRow) => {\n            const result = (searchState.results ?? [])[virtualRow.index];\n\n            return (\n              <SearchItem\n                key={result.item.toString()}\n                itemProps={cb.getItemProps({\n                  item: result,\n                  index: virtualRow.index,\n                  style: {\n                    position: \"absolute\",\n                    top: 0,\n                    left: 0,\n                    width: \"100%\",\n                    height: virtualRow.size,\n                    transform: `translateY(${virtualRow.start}px)`,\n                  },\n                })}\n                result={result}\n                isHighlighted={virtualRow.index === cb.highlightedIndex}\n              />\n            );\n          })}\n        </ul>\n      </div>\n      <div className=\"flex items-center w-full gap-4 px-3 py-2 border-t-[1px] bg-slate-100 border-slate-200 rounded-br-lg rounded-bl-lg transition dark:bg-slate-900 dark:border-slate-700\">\n        <div className=\"flex items-center gap-1\">\n          <ShortcutIcon className=\"w-4 h-4 text-sm text-slate-900 bg-slate-300 transition duration-75 group-hover:bg-slate-100 dark:bg-slate-500 dark:group-hover:bg-slate-600\">\n            ⏎\n          </ShortcutIcon>\n          <Body className=\"text-slate-700 dakr:text-slate-500\">to select</Body>\n        </div>\n        <div className=\"flex items-center gap-1\">\n          <ArrowKeysUpDownIcon className=\"transition text-slate-300 dark:text-slate-500\" />\n          <Body className=\"text-slate-700 dakr:text-slate-500\">\n            to navigate\n          </Body>\n        </div>\n        <div className=\"flex items-center gap-1\">\n          <EscapeKeyIcon className=\"transition text-slate-300 dark:text-slate-500\" />\n          <Body className=\"text-slate-700 dakr:text-slate-500\">to close</Body>\n        </div>\n      </div>\n    </>\n  );\n}\n\ntype SearchItemProps = {\n  itemProps: React.HTMLAttributes<HTMLLIElement>;\n  result: SearchResult<string>;\n  isHighlighted: boolean;\n};\n\nexport function SearchItem({\n  itemProps,\n  result,\n  isHighlighted,\n}: SearchItemProps) {\n  const heroPath = new JSONHeroPath(result.item);\n  const [json] = useJson();\n\n  const itemValue = heroPath.first(json);\n  const ItemIcon = iconForValue(itemValue);\n\n  return (\n    <li\n      {...itemProps}\n      className={classnames(\"flex w-full hover:cursor-pointer\")}\n    >\n      <div\n        className={classnames(\n          \"w-full h-[calc(100%-4px)] mb-2 rounded-sm group\",\n          isHighlighted ? \"bg-indigo-700\" : \"bg-slate-100 dark:bg-slate-900\"\n        )}\n      >\n        <div className=\"flex items-center w-full py-2 pl-4 pr-3\">\n          <ItemIcon\n            className={classnames(\n              \"h-6 w-6\",\n              isHighlighted\n                ? \"text-white\"\n                : \"text-slate-500 dark:text-slate-400\"\n            )}\n          ></ItemIcon>\n          <div className=\"flex flex-col ml-3\">\n            <div className=\"flex w-full items-baseline\">\n              <SearchPathResult\n                path={heroPath}\n                searchResult={result}\n                isHighlighted={isHighlighted}\n              />\n            </div>\n            <div className=\"key-value flex justify-between\">\n              {result.score.rawValue && (\n                <SearchResultValue\n                  isHighlighted={isHighlighted}\n                  stringValue={result.score.rawValue}\n                  matches={result.score.rawValueMatch}\n                />\n              )}\n              {result.score.formattedValue &&\n                result.score.formattedValue !== result.score.rawValue && (\n                  <SearchResultValue\n                    isHighlighted={isHighlighted}\n                    stringValue={result.score.formattedValue}\n                    matches={result.score.formattedValueMatch}\n                  />\n                )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </li>\n  );\n}\n\n// Outputs the following pair for each component except for the last one:\n// <Body className=\"text-lg\">{component}</Body>,\n// <ChevronRightIcon className=\"w-4 h-4\" />,\n//\n// Highlights parts of the component that match the search query.\n// The match indices match against the stringified version of the path (e.g. $.foo.bar.0.details.description)\n//\n// If combined component strings are too long, then we need to choose some components to hide behind an ellipsis, making sure we don't hide matches\nfunction SearchPathResult({\n  path,\n  searchResult,\n  isHighlighted,\n  maxWeight = 90,\n}: {\n  path: JSONHeroPath;\n  isHighlighted: boolean;\n  searchResult: SearchResult<string>;\n  maxWeight?: number;\n}) {\n  const description = searchResult.score.description;\n  const label = searchResult.score.label;\n\n  const labelMatches = searchResult.score.labelMatch;\n  const descriptionMatches = searchResult.score.descriptionMatch;\n\n  const descriptionSlices = getComponentSlices(\n    description ?? \"\",\n    (descriptionMatches ?? []).map(({ start, end }) => ({\n      start,\n      end: end - 1,\n    })),\n    maxWeight\n  );\n\n  return (\n    <>\n      {label && (\n        <SearchResultValue\n          isHighlighted={isHighlighted}\n          stringValue={label}\n          matches={labelMatches}\n          textSize=\"text-lg\"\n          className={classnames(\n            \"mr-3 text-lg\",\n            isHighlighted ? `text-white` : \"text-slate-900 dark:text-white\"\n          )}\n          key=\"label\"\n        />\n      )}\n      {descriptionSlices.map((slice, i) =>\n        slice.type === \"component\" ? (\n          <span\n            key={i}\n            className={\n              slice.slice.isMatch\n                ? classnames(\n                    \"text-base\",\n                    isHighlighted\n                      ? \"text-white underline underline-offset-1\"\n                      : \"text-indigo-600 dark:text-indigo-400\"\n                  )\n                : classnames(\n                    \"text-base\",\n                    isHighlighted\n                      ? \"text-white\"\n                      : \"text-slate-800 dark:text-slate-400\"\n                  )\n            }\n          >\n            {slice.slice.slice}\n          </span>\n        ) : slice.type === \"ellipsis\" ? (\n          <Body\n            key={i}\n            className={classnames(\n              \"text-base\",\n              isHighlighted\n                ? \"text-white\"\n                : \"text-slate-600 dark:text-slate-400\"\n            )}\n          >\n            …\n          </Body>\n        ) : (\n          <ChevronRightIcon\n            key={i}\n            className={classnames(\n              \"w-3 h-3 mx-[1px] relative top-[2px]\",\n              isHighlighted\n                ? \"text-white\"\n                : \"text-slate-600 dark:text-slate-400\"\n            )}\n          />\n        )\n      )}\n    </>\n  );\n}\n\nfunction SearchResultValue({\n  isHighlighted,\n  stringValue,\n  matches,\n  className,\n  textSize,\n}: {\n  isHighlighted: boolean;\n  stringValue: string;\n  matches?: Array<Match>;\n  className?: string;\n  textSize?: \"text-xs\" | \"text-sm\" | \"text-base\" | \"text-lg\";\n}) {\n  const output = createOutputForMatch(\n    stringValue,\n    isHighlighted,\n    textSize,\n    matches\n  );\n\n  return (\n    <Body\n      className={\n        className ??\n        classnames(\n          \"mr-2\",\n          isHighlighted ? `text-white` : \"text-slate-600 dark:text-slate-400\"\n        )\n      }\n    >\n      {output}\n    </Body>\n  );\n}\n\nfunction createOutputForMatch(\n  stringValue: string,\n  isHighlighted: boolean,\n  textSize: \"text-xs\" | \"text-sm\" | \"text-base\" | \"text-lg\" = \"text-base\",\n  matches?: Array<Match>,\n  maxLength: number = 68\n): JSX.Element {\n  if (!matches || matches.length === 0) {\n    return <>{truncate(stringValue, { length: maxLength })}</>;\n  }\n\n  const stringSlices = getStringSlices(stringValue, matches, maxLength);\n\n  return (\n    <>\n      {stringSlices.map((s, index) => {\n        return (\n          <span\n            key={index}\n            className={\n              s.isMatch\n                ? classnames(\n                    textSize,\n                    isHighlighted\n                      ? \"text-white underline underline-offset-1\"\n                      : \"text-indigo-600 dark:text-indigo-400\"\n                  )\n                : \"\"\n            }\n          >\n            {s.slice}\n          </span>\n        );\n      })}\n    </>\n  );\n}\n"
  },
  {
    "path": "app/components/Share.tsx",
    "content": "import React, { useCallback, useEffect, useState } from \"react\";\nimport { Body } from \"./Primitives/Body\";\nimport { ClipboardIcon } from \"@heroicons/react/outline\";\nimport { useJsonColumnViewState } from \"~/hooks/useJsonColumnView\";\n\nconst buttonDefault = (\n  <>\n    <ClipboardIcon className=\"h-4 w-4 mr-[2px]\" />\n    <span>Copy</span>\n  </>\n);\n\nexport function Share() {\n  useEffect(() => {\n    setLink(window.location.href);\n  }, []);\n  const [link, setLink] = useState(\"\");\n\n  const [copyText, setCopyText] = useState<React.ReactNode>(buttonDefault);\n\n  const [copied, setCopied] = useState(false);\n\n  useEffect(() => {\n    if (copied) {\n      const timeout = setTimeout(() => {\n        setCopyText(buttonDefault);\n        setCopied(false);\n      }, 1800);\n\n      return () => clearTimeout(timeout);\n    }\n  }, [copied]);\n\n  const handleCopy = useCallback(() => {\n    navigator.clipboard.writeText(link).then(\n      function () {\n        setCopyText(<span>Copied!</span>);\n        setCopied(true);\n      },\n      function (err) {\n        setCopyText(<span>Failed to copy</span>);\n        setCopied(true);\n      }\n    );\n  }, [link, setCopyText]);\n\n  const { selectedNodeId } = useJsonColumnViewState();\n\n  const handleIncludesPath = useCallback(\n    (includesPath: boolean) => {\n      if (!selectedNodeId) {\n        return;\n      }\n\n      if (includesPath) {\n        const url = new URL(window.location.href);\n        for (const [key] of url.searchParams) {\n          url.searchParams.delete(key);\n        }\n\n        url.searchParams.append(\"path\", selectedNodeId);\n\n        setLink(url.href);\n      } else {\n        setLink(window.location.href);\n      }\n    },\n    [link, selectedNodeId]\n  );\n\n  return (\n    <div className=\"bg-indigo-700 text-white rounded-sm shadow-md w-[340px] p-3 transition\">\n      <Body className=\"text-sm mb-2 text-slate-300\">\n        Anyone with this link can view this json file.\n      </Body>\n      <div className=\"flex\">\n        <div className=\"flex-grow whitespace-nowrap overflow-hidden rounded-l-sm bg-indigo-900 text-sm p-2 select-all\">\n          {link}\n        </div>\n        <div\n          className=\"flex items-center justify-center text-lg text-slate-800 min-w-[74px] bg-white bg-opacity-80 rounded-r-sm transition hover:bg-opacity-100 cursor-pointer\"\n          onClick={handleCopy}\n        >\n          {copyText}\n        </div>\n      </div>\n      <div className=\"form-check form-check-inline mt-2\">\n        <label className=\"flex items-center text-sm form-check-label text-slate-300 select-none hover:cursor-pointer transition\">\n          <input\n            className=\"form-check-input appearance-none h-4 w-4 border border-slate-300 rounded-sm bg-white checked:bg-indigo-700 checked:border-indigo-700 focus:outline-none duration-200 align-top bg-no-repeat bg-center bg-contain float-left mr-2 hover:cursor-pointer transition dark:border-slate-300 dark:bg-slate-200 dark:checked:bg-lime-500 dark:checked:border-lime-500\"\n            type=\"checkbox\"\n            id=\"inlineCheckbox\"\n            value=\"option\"\n            onChange={(e) => handleIncludesPath(e.target.checked)}\n          ></input>\n          Link includes path\n        </label>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/SideBar.tsx",
    "content": "import { TemplateIcon, CodeIcon, DownloadIcon } from \"@heroicons/react/outline\";\nimport { TreeIcon } from \"~/components/Icons/TreeIcon\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\nimport { Link, useLocation, useNavigate } from \"remix\";\nimport { useJsonDoc } from \"~/hooks/useJsonDoc\";\nimport { ToolTip } from \"./ToolTip\";\nimport { Body } from \"./Primitives/Body\";\nimport { ShortcutIcon } from \"./Icons/ShortcutIcon\";\nimport { useTheme } from \"./ThemeProvider\";\n\nexport function SideBar() {\n  const { doc } = useJsonDoc();\n\n  return (\n    <div className=\"side-bar flex flex-col align-center justify-between h-full p-1 bg-slate-200 transition dark:bg-slate-800\">\n      <ol className=\"relative\">\n        <SidebarLink to={`/j/${doc.id}`} hotKey=\"option+1,alt+1\">\n          <ToolTip arrow=\"left\">\n            <Body>Column view</Body>\n            <ShortcutIcon className=\"w-[26px] h-[26px] ml-1 text-slate-700 bg-slate-200 dark:text-slate-300 dark:bg-slate-800\">\n              ⌥\n            </ShortcutIcon>\n            <ShortcutIcon className=\"w-[26px] h-[26px] ml-1 text-slate-700 bg-slate-200 dark:text-slate-300 dark:bg-slate-800\">\n              1\n            </ShortcutIcon>\n          </ToolTip>\n          <TemplateIcon className=\"p-2 w-full h-full\" />\n        </SidebarLink>\n        <SidebarLink to={`/j/${doc.id}/editor`} hotKey=\"option+2,alt+2\">\n          <ToolTip arrow=\"left\">\n            <Body>JSON view</Body>\n            <ShortcutIcon className=\"w-[26px] h-[26px] ml-1 text-slate-700 bg-slate-200 dark:text-slate-300 dark:bg-slate-800\">\n              ⌥\n            </ShortcutIcon>\n            <ShortcutIcon className=\"w-[26px] h-[26px] ml-1 text-slate-700 bg-slate-200 dark:text-slate-300 dark:bg-slate-800\">\n              2\n            </ShortcutIcon>\n          </ToolTip>\n          <CodeIcon className=\"p-2 w-full h-full\" />\n        </SidebarLink>\n        <SidebarLink to={`/j/${doc.id}/tree`} hotKey=\"option+3,alt+3\">\n          <ToolTip arrow=\"left\">\n            <Body>Tree view</Body>\n            <ShortcutIcon className=\"w-[26px] h-[26px] ml-1 text-slate-700 bg-slate-200 dark:text-slate-300 dark:bg-slate-800\">\n              ⌥\n            </ShortcutIcon>\n            <ShortcutIcon className=\"w-[26px] h-[26px] ml-1 text-slate-700 bg-slate-200 dark:text-slate-300 dark:bg-slate-800\">\n              3\n            </ShortcutIcon>\n          </ToolTip>\n          <TreeIcon className=\"p-2 w-full h-full\" />\n        </SidebarLink>\n      </ol>\n      <ol>\n        <SidebarLink>\n          <a href={`/j/${doc.id}.json`} target=\"_blank\">\n            <ToolTip arrow=\"left\">\n              <Body>Download</Body>\n            </ToolTip>\n            <DownloadIcon className=\"p-2 w-full h-full\" />\n          </a>\n        </SidebarLink>\n      </ol>\n    </div>\n  );\n}\n\nfunction SidebarLink({\n  children,\n  to,\n  hotKey,\n}: {\n  children: React.ReactNode;\n  to?: string;\n  hotKey?: string;\n}) {\n  const location = useLocation();\n\n  const isActive = location.pathname === to;\n\n  const { minimal } = useJsonDoc();\n  const [theme] = useTheme();\n\n  const queryParams = new URLSearchParams();\n\n  if (typeof minimal === \"boolean\") {\n    queryParams.set(\"minimal\", String(minimal));\n\n    if (theme) {\n      queryParams.set(\"theme\", theme);\n    }\n  }\n\n  const href = `${to}${queryParams.toString().length > 0 ? `?${queryParams.toString()}` : \"\"\n    }`;\n\n  if (hotKey) {\n    const navigate = useNavigate();\n    useHotkeys(\n      hotKey,\n      (e) => {\n        e.preventDefault();\n        if (!isActive && to) {\n          navigate(href);\n        }\n      },\n      [navigate, isActive, to]\n    );\n  }\n\n  const classes = isActive\n    ? \"relative w-10 h-10 mb-1 text-white bg-indigo-700 rounded-sm cursor:pointer transition\"\n    : \"relative w-10 h-10 mb-1 text-slate-700 hover:bg-slate-300 rounded-sm cursor:pointer transition dark:text-white dark:hover:bg-slate-700\";\n\n  return !!to ? (\n    <Link to={href} prefetch={isActive ? \"none\" : \"render\"}>\n      <li className={classes}>{children}</li>\n    </Link>\n  ) : (\n    <li className={classes}>{children}</li>\n  );\n}\n"
  },
  {
    "path": "app/components/StarCountProvider.tsx",
    "content": "import { createContext, useContext } from \"react\";\nimport type { ReactNode } from \"react\";\n\nexport type StarCountType = number | undefined;\n\nconst StarCountContext = createContext<StarCountType>(undefined);\n\nexport function StarCountProvider({\n  children,\n  starCount,\n}: {\n  children: ReactNode;\n  starCount: StarCountType;\n}) {\n  return (\n    <StarCountContext.Provider value={starCount}>\n      {children}\n    </StarCountContext.Provider>\n  );\n}\n\nexport function useStarCount(): StarCountType {\n  return useContext(StarCountContext);\n}\n"
  },
  {
    "path": "app/components/ThemeModeToggle.tsx",
    "content": "import { useHotkeys } from \"react-hotkeys-hook\";\nimport { MoonIcon } from \"./Icons/MoonIcon\";\nimport { SunIcon } from \"./Icons/SunIcon\";\nimport { useTheme } from \"./ThemeProvider\";\n\nexport function ThemeModeToggler() {\n  const [theme, setTheme] = useTheme();\n\n  const toggleTheme = () => {\n    setTheme((prevTheme) => (prevTheme === \"light\" ? \"dark\" : \"light\"));\n  };\n  const SwitchIcon = theme === \"light\" ? MoonIcon : SunIcon;\n\n  useHotkeys(\"alt+t\", () => toggleTheme(), [toggleTheme]);\n\n  return (\n    <button\n      className={`flex text-xl items-center px-2 py-1.5 transition ${\n        theme === \"light\"\n          ? \"text-slate-800 hover:bg-slate-300\"\n          : \"text-white hover:bg-slate-700\"\n      }`}\n      onClick={toggleTheme}\n    >\n      <SwitchIcon />\n    </button>\n  );\n}\n"
  },
  {
    "path": "app/components/ThemeProvider.tsx",
    "content": "import { createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport type { Dispatch, ReactNode, SetStateAction } from \"react\";\nimport { useFetcher } from \"remix\";\n\nexport type Theme = \"dark\" | \"light\";\n\ntype ThemeContextType = [\n  Theme | undefined,\n  Dispatch<SetStateAction<Theme | undefined>>\n];\n\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n\nconst prefersLightMQ = \"(prefers-color-scheme: light)\";\nconst getPreferredTheme = () =>\n  window.matchMedia(prefersLightMQ).matches ? \"light\" : \"dark\";\n\nexport function ThemeProvider({\n  children,\n  specifiedTheme,\n  themeOverride,\n}: {\n  children: ReactNode;\n  specifiedTheme?: Theme;\n  themeOverride?: Theme;\n}) {\n  const [theme, setTheme] = useState<Theme | undefined>(() => {\n    if (specifiedTheme) {\n      if (specifiedTheme === \"light\" || specifiedTheme === \"dark\") {\n        return specifiedTheme;\n      } else {\n        return;\n      }\n    }\n\n    // there's no way for us to know what the theme should be in this context\n    // the client will have to figure it out before hydration.\n    if (typeof window !== \"object\") {\n      return;\n    }\n\n    return getPreferredTheme();\n  });\n\n  const mountRun = useRef(false);\n  const persistTheme = useFetcher();\n\n  useEffect(() => {\n    if (!mountRun.current) {\n      mountRun.current = true;\n      return;\n    }\n\n    if (!theme) {\n      return;\n    }\n\n    persistTheme.submit(\n      { theme },\n      { action: \"actions/setTheme\", method: \"post\" }\n    );\n  }, [theme]);\n\n  return (\n    <ThemeContext.Provider value={[themeOverride ?? theme, setTheme]}>\n      {children}\n    </ThemeContext.Provider>\n  );\n}\n\nexport function useTheme(): ThemeContextType {\n  const context = useContext(ThemeContext);\n  if (context === undefined) {\n    throw new Error(\"useTheme must be used within a ThemeProvider\");\n  }\n  return context;\n}\n\nconst clientThemeCode = `\n;(() => {\n  const theme = window.matchMedia(${JSON.stringify(prefersLightMQ)}).matches\n    ? 'light'\n    : 'dark';\n  const cl = document.documentElement.classList;\n  const themeAlreadyApplied = cl.contains('light') || cl.contains('dark');\n  if (themeAlreadyApplied) {\n    // this script shouldn't exist if the theme is already applied!\n    console.warn(\n      \"Hi there, could you let us know you're seeing this message? Thanks!\",\n    );\n  } else {\n    cl.add(theme);\n  }\n})();\n`;\n\nexport function NonFlashOfWrongThemeEls({ ssrTheme }: { ssrTheme: boolean }) {\n  return (\n    <>\n      {ssrTheme ? null : (\n        <script dangerouslySetInnerHTML={{ __html: clientThemeCode }} />\n      )}\n    </>\n  );\n}\n\nexport function isTheme(value: unknown): value is Theme {\n  return typeof value === \"string\" && [\"light\", \"dark\"].includes(value);\n}\n"
  },
  {
    "path": "app/components/ToolTip.tsx",
    "content": "import React, { useState } from \"react\";\nimport { motion } from \"framer-motion\";\n\nexport type ToolTipProps = {\n  children: React.ReactNode;\n  className?: string;\n  arrow?: ArrowDirection;\n};\n\nexport type ArrowDirection = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport function ToolTip({ children, className, arrow }: ToolTipProps) {\n  const [isShown, setIsShown] = useState(false);\n  const arrowStyle = () => {\n    if (!arrow) {\n      return \"\";\n    }\n    switch (arrow) {\n      case \"top\":\n        return \"top-[40px] after:bg-white after:border-[1px] after:border-t-slate-300 after:border-r-transparent after:border-b-transparent after:border-l-slate-300 after:dark:border-t-slate-600 after:dark:border-r-transprent after:dark:border-b-transparent after:dark:border-l-slate-600 after:dark:bg-slate-700 after:h-[14px] after:w-[14px] after:top-[-8px] after:left-[calc(50%-7px)] after:content-[''] after:absolute after:bg-white after:rotate-45\";\n      case \"bottom\":\n        return \"bottom-[49px] after:bg-white after:border-[1px] after:border-t-transparent after:border-r-transparent after:border-b-slate-300 after:border-l-slate-300 after:dark:border-t-transprent after:dark:border-r-transprent after:dark:border-b-slate-600 after:dark:border-l-slate-600 after:dark:bg-slate-700 after:h-[14px] after:w-[14px] after:left-[-8px] after:top-[calc(50%-7px)] after:content-[''] after:absolute after:bg-white after:rotate-45\";\n      case \"left\":\n        return \"left-[49px] after:bg-white after:border-[1px] after:border-t-transparent after:border-r-transparent after:border-b-slate-300 after:border-l-slate-300 after:dark:border-t-transprent after:dark:border-r-transprent after:dark:border-b-slate-600 after:dark:border-l-slate-600 after:dark:bg-slate-700 after:h-[14px] after:w-[14px] after:left-[-8px] after:top-[calc(50%-7px)] after:content-[''] after:absolute after:bg-white after:rotate-45\";\n      case \"right\":\n        return \"right-[49px] after:bg-white after:border-[1px] after:border-t-transparent after:border-r-transparent after:border-b-slate-300 after:border-l-slate-300 after:dark:border-t-transprent after:dark:border-r-transprent after:dark:border-b-slate-600 after:dark:border-l-slate-600 after:dark:bg-slate-700 after:h-[14px] after:w-[14px] after:left-[-8px] after:top-[calc(50%-7px)] after:content-[''] after:absolute after:bg-white after:rotate-45\";\n    }\n  };\n\n  return (\n    <motion.div\n      animate={{}}\n      initial={{ scale: 0.97, opacity: 0.5 }}\n      transition={{ duration: 0.2 }}\n      whileHover={{ scale: 1, opacity: 1 }}\n      whileTap={{\n        scale: 1,\n      }}\n      onMouseOver={() => setIsShown(true)}\n      onMouseOut={() => setIsShown(false)}\n      className={`${className} absolute flex justify-center top-0 text-center z-10 h-full w-full text-slate-800 transtition dark:text-slate-200`}\n    >\n      <div\n        className={`absolute flex items-center ${\n          isShown\n            ? `${arrowStyle()} pl-3 pr-2 py-2 w-max shadow rounded-sm border-slate-300 border-[1px] bg-white dark:bg-slate-700  dark:border-slate-600`\n            : \"\"\n        }`}\n      >\n        {isShown && children}\n      </div>\n    </motion.div>\n  );\n}\n"
  },
  {
    "path": "app/components/UI/Dialog.tsx",
    "content": "import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport React, { ComponentPropsWithoutRef } from \"react\";\nimport { omit } from \"lodash-es\";\n\nexport const DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {\n    onOverlayClick?: () => void;\n  }\n>(({ children, ...props }, forwardedRef) => (\n  <DialogPrimitive.Portal>\n    <DialogPrimitive.Overlay\n      forceMount\n      className=\"fixed inset-0 z-20 bg-black/60\"\n      onClick={() => {\n        props.onOverlayClick && props.onOverlayClick();\n      }}\n    />\n    <DialogPrimitive.Content\n      forceMount\n      {...omit(props, \"onOverlayClick\")}\n      ref={forwardedRef}\n    >\n      {children}\n    </DialogPrimitive.Content>\n  </DialogPrimitive.Portal>\n));\n\nexport const Dialog = DialogPrimitive.Root;\nexport const DialogTrigger = DialogPrimitive.Trigger;\n"
  },
  {
    "path": "app/components/UI/GithubStar.tsx",
    "content": "import { formatStarCount } from \"~/utilities/formatStarCount\";\nimport { GithubIconSimple } from \"../Icons/GithubIconSimple\";\nimport { Body } from \"../Primitives/Body\";\nimport { useStarCount } from \"../StarCountProvider\";\n\nexport type GithubStarProps = {\n  className?: string;\n};\n\nexport function GithubStar({ className }: GithubStarProps) {\n  const starCount = useStarCount();\n\n  return (\n    <a\n      href=\"https://github.com/triggerdotdev/jsonhero-web\"\n      target=\"_blank\"\n      className=\"flex text-slate-700 opacity-90 transition hover:cursor-pointer hover:opacity-100\"\n    >\n      <div className=\"flex items-center gap-1 pr-2 pl-1 py-1 bg-slate-300 rounded-l\">\n        <GithubIconSimple className=\"w-4 h-4 ml-1\"></GithubIconSimple>\n        <Body className=\"font-semibold text-slate-800 hidden md:block\">\n          Star\n        </Body>\n      </div>\n      {starCount && (\n        <div className=\"px-2 py-1 border-l border-slate-400 bg-slate-100 rounded-r\">\n          <Body className=\"font-bold\">{formatStarCount(starCount)}</Body>\n        </div>\n      )}\n    </a>\n  );\n}\n"
  },
  {
    "path": "app/components/UI/GithubStarSmall.tsx",
    "content": "import { formatStarCount } from \"~/utilities/formatStarCount\";\nimport { GithubIconSimple } from \"../Icons/GithubIconSimple\";\nimport { Body } from \"../Primitives/Body\";\nimport { useStarCount } from \"../StarCountProvider\";\n\nexport type GithubStarSmallProps = {\n  className?: string;\n};\n\nexport function GithubStarSmall({ className }: GithubStarSmallProps) {\n  const starCount = useStarCount();\n\n  return (\n    <a\n      href=\"https://github.com/triggerdotdev/jsonhero-web\"\n      target=\"_blank\"\n      className=\"flex p-1 text-slate-700 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-700 transition hover:cursor-pointer\"\n    >\n      <div className=\"flex items-center gap-1.5 pl-1 rounded-l-sm\">\n        <GithubIconSimple className=\"w-4 h-4 ml-1 text-slate-700 dark:text-white transition\"></GithubIconSimple>\n        <Body className=\"font-semibold text-slate-800 dark:text-slate-100\">\n          Star\n        </Body>\n      </div>\n      {starCount && (\n        <div className=\"pr-2 pl-1\">\n          <Body className=\"font-bold dark:text-slate-100\">\n            {formatStarCount(starCount)}\n          </Body>\n        </div>\n      )}\n    </a>\n  );\n}\n"
  },
  {
    "path": "app/components/UI/Popover.tsx",
    "content": "import * as PopoverPrimitive from \"@radix-ui/react-popover\";\nimport type { ComponentPropsWithoutRef } from \"@radix-ui/react-primitive\";\nimport React from \"react\";\n\nexport const Popover = PopoverPrimitive.Root;\n\nexport const PopoverTrigger = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Trigger>,\n  ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger>\n>((props, ref) => {\n  return <PopoverPrimitive.Trigger asChild ref={ref} {...props} />;\n});\n\nexport const PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ children, ...props }, ref) => {\n  return (\n    <PopoverPrimitive.Content {...props} ref={ref}>\n      {children}\n    </PopoverPrimitive.Content>\n  );\n});\n\nexport const PopoverArrow = PopoverPrimitive.Arrow;\n"
  },
  {
    "path": "app/components/UI/Tabs.tsx",
    "content": "import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport type { ComponentPropsWithoutRef } from \"@radix-ui/react-primitive\";\nimport React from \"react\";\nimport cx from \"~/utilities/classnames\";\n\nexport type TabProps = {\n  tabs: Array<{ value: string; label: string }>;\n  children: React.ReactNode;\n};\n\nexport function Tabs({ tabs, children }: TabProps) {\n  return (\n    <TabsPrimitive.Root defaultValue={tabs[0].value}>\n      <TabsPrimitive.List className=\"\">\n        {tabs.map(({ value, label }) => (\n          <TabsPrimitive.Trigger\n            value={value}\n            key={`tab-trigger-${value}`}\n            className={cx(\n              \"group\",\n              \"mr-1 px-4 py-1 rounded-t-sm transition\",\n              \"text-slate-500 hover:bg-slate-100 hover:bg-opacity-50 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:bg-opacity-40\",\n              \"radix-state-active:bg-slate-100 radix-state-active:bg-opacity-50 radix-state-active:dark:bg-slate-900 radix-state-active:dark:text-white\"\n            )}\n          >\n            {label}\n          </TabsPrimitive.Trigger>\n        ))}\n      </TabsPrimitive.List>\n      {children}\n    </TabsPrimitive.Root>\n  );\n}\n\nexport const TabContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>((props, ref) => {\n  return <TabsPrimitive.TabsContent ref={ref} {...props} />;\n});\n"
  },
  {
    "path": "app/components/UI/ToastPopover.tsx",
    "content": "import {\n  ExclamationCircleIcon,\n  InformationCircleIcon,\n} from \"@heroicons/react/outline\";\nimport * as ToastPrimitive from \"@radix-ui/react-toast\";\nimport cx from \"~/utilities/classnames\";\nimport { Body } from \"../Primitives/Body\";\nimport { Title } from \"../Primitives/Title\";\n\nconst Toast = ({\n  message,\n  title,\n  duration,\n  type,\n}: {\n  message: string;\n  title: string;\n  type: \"success\" | \"error\";\n  duration?: number;\n}) => {\n  const commonRootClasses = cx(\n    \"z-50 fixed top-4 left-4 py-2 w-auto md:top-4 md:right-4 md:left-auto md:top-auto md:w-full md:max-w-sm shadow-lg rounded-md\",\n    \"border-[1px]\",\n    \"radix-state-open:animate-toast-slide-in-top md:radix-state-open:animate-toast-slide-in-right\",\n    \"radix-state-closed:animate-toast-hide\",\n    \"radix-swipe-end:animate-toast-swipe-out\",\n    \"translate-x-radix-toast-swipe-move-x\",\n    \"radix-swipe-cancel:translate-x-0 radix-swipe-cancel:duration-200 radix-swipe-cancel:ease-[ease]\",\n    \"focus:outline-none focus-visible:ring focus-visible:ring-indigo-500 focus-visible:ring-opacity-75\"\n  );\n\n  const typeRootClasses =\n    type === \"success\"\n      ? \"bg-slate-50 dark:bg-slate-900\"\n      : \"bg-rose-50 dark:bg-rose-100\";\n\n  const titleClasses =\n    type === \"success\" ? \"text-emerald-500\" : \"text-slate-900\";\n  const bodyClasses =\n    type === \"success\" ? \"text-emerald-500\" : \"text-slate-700\";\n\n  const iconType =\n    type === \"success\" ? \"text-emerald-700 h-7 w-7\" : \"text-rose-700 h-7 w-7\";\n\n  return (\n    <ToastPrimitive.Provider duration={duration ?? 2500}>\n      <ToastPrimitive.Root className={cx(commonRootClasses, typeRootClasses)}>\n        <div className=\"flex\">\n          <div className=\"flex-1 flex items-center\">\n            <div className=\"flex px-4\">\n              {type === \"success\" ? (\n                <InformationCircleIcon className={cx(iconType)} />\n              ) : (\n                <ExclamationCircleIcon className={cx(iconType)} />\n              )}\n            </div>\n            <div className=\"w-full radix\">\n              <Title className={cx(\"-mb-0.5\", titleClasses)}>{title}</Title>\n              <Body className={cx(\"mb-0.5\", bodyClasses)}>{message}</Body>\n            </div>\n          </div>\n        </div>\n      </ToastPrimitive.Root>\n\n      <ToastPrimitive.Viewport />\n    </ToastPrimitive.Provider>\n  );\n};\n\nexport default Toast;\n"
  },
  {
    "path": "app/components/UrlForm.tsx",
    "content": "import { useState } from \"react\";\nimport { Form, useTransition } from \"remix\";\n\nexport type UrlFormProps = {\n  className?: string;\n};\n\nexport function UrlForm({ className }: UrlFormProps) {\n  const transition = useTransition();\n  const [inputValue, setInputValue] = useState(\"\");\n\n  const isNotIdle = transition.state !== \"idle\";\n  const isButtonDisabled = !inputValue.length || isNotIdle;\n\n  return (\n    <Form\n      method=\"post\"\n      action=\"/actions/createFromUrl\"\n      className={`${className}`}\n    >\n      <div className=\"flex\">\n        <input\n          type=\"text\"\n          name=\"jsonUrl\"\n          id=\"jsonUrl\"\n          className=\"block flex-grow text-base text-slate-200 placeholder:text-slate-300 bg-slate-900/40 border border-slate-600 rounded-l-sm py-2 px-3 transition duration-300 focus:ring-indigo-500 focus:border-indigo-500\"\n          placeholder=\"Enter a JSON URL or paste in JSON here...\"\n          value={inputValue}\n          onChange={(event) => setInputValue(event.target.value)}\n        />\n        <button\n          type=\"submit\"\n          value=\"Go\"\n          className={`inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-r-sm text-white bg-lime-500 transition hover:bg-lime-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-lime-500 ${\n            isButtonDisabled && \"disabled:opacity-50 disabled:hover:bg-lime-500\"\n          }`}\n          disabled={isButtonDisabled}\n        >\n          {isNotIdle ? \"...\" : \"Go\"}\n        </button>\n      </div>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "app/components/ValueIcon.tsx",
    "content": "import { FunctionComponent } from \"react\";\nimport {\n  ArchiveIcon,\n  AtSymbolIcon,\n  CalendarIcon,\n  ChatAlt2Icon,\n  CheckCircleIcon,\n  ClockIcon,\n  CodeIcon,\n  CollectionIcon,\n  ColorSwatchIcon,\n  CreditCardIcon,\n  CubeIcon,\n  CurrencyDollarIcon,\n  DocumentTextIcon,\n  EmojiHappyIcon,\n  EyeOffIcon,\n  GlobeAltIcon,\n  GlobeIcon,\n  HashtagIcon,\n  IdentificationIcon,\n  KeyIcon,\n  PhoneIcon,\n  PhotographIcon,\n} from \"@heroicons/react/outline\";\nimport { JSONValueType } from \"@jsonhero/json-infer-types\";\nimport { colorForTypeName } from \"../utilities/colors\";\nimport { StringIcon } from \"./Icons/StringIcon\";\n\ntype ValueIconProps = {\n  type: JSONValueType;\n  size?: ValueIconSize;\n  monochrome?: boolean;\n};\n\nexport enum ValueIconSize {\n  Small,\n  Medium,\n}\n\nexport const ValueIcon: FunctionComponent<ValueIconProps> = ({\n  type,\n  size = ValueIconSize.Small,\n  monochrome = false,\n}) => {\n  let classes = monochrome ? `text-slate-300` : colorForTypeName(type.name);\n  switch (size) {\n    case ValueIconSize.Small:\n      classes += \" h-4 w-4\";\n      break;\n    case ValueIconSize.Medium:\n      classes += \" h-6 w-6\";\n      break;\n  }\n\n  switch (type.name) {\n    case \"object\": {\n      return <CubeIcon className={classes} />;\n    }\n    case \"array\": {\n      return <CollectionIcon className={classes} />;\n    }\n    case \"null\": {\n      return <EyeOffIcon className={classes} />;\n    }\n    case \"bool\": {\n      return <CheckCircleIcon className={classes} />;\n    }\n    case \"int\": {\n      if (type.format == null) {\n        return <HashtagIcon className={classes} />;\n      }\n      switch (type.format.name) {\n        case \"timestamp\": {\n          return <CalendarIcon className={classes} />;\n        }\n      }\n    }\n    case \"float\": {\n      return <HashtagIcon className={classes} />;\n    }\n    case \"string\": {\n      if (type.format == null) {\n        return <StringIcon className={classes} />;\n      }\n\n      switch (type.format.name) {\n        case \"timestamp\": {\n          return <CalendarIcon className={classes} />;\n        }\n        case \"datetime\": {\n          switch (type.format.parts) {\n            case \"time\":\n              return <ClockIcon className={classes} />;\n          }\n          return <CalendarIcon className={classes} />;\n        }\n        case \"email\": {\n          return <AtSymbolIcon className={classes} />;\n        }\n        case \"hostname\":\n        case \"tld\":\n        case \"ip\": {\n          return <GlobeAltIcon className={classes} />;\n        }\n        case \"uri\": {\n          switch (type.format.contentType) {\n            case \"image/jpeg\":\n            case \"image/png\":\n            case \"image/gif\":\n            case \"image/webm\":\n              return <PhotographIcon className={classes} />;\n            case \"application/json\":\n              return <CodeIcon className={classes} />;\n            default:\n              return <GlobeAltIcon className={classes} />;\n          }\n        }\n        case \"phoneNumber\": {\n          return <PhoneIcon className={classes} />;\n        }\n        case \"currency\": {\n          return <CurrencyDollarIcon className={classes} />;\n        }\n        case \"country\": {\n          return <GlobeIcon className={classes} />;\n        }\n        case \"emoji\": {\n          return <EmojiHappyIcon className={classes} />;\n        }\n        case \"language\": {\n          return <ChatAlt2Icon className={classes} />;\n        }\n        case \"filesize\": {\n          return <ArchiveIcon className={classes} />;\n        }\n        case \"uuid\": {\n          return <IdentificationIcon className={classes} />;\n        }\n        case \"json\":\n        case \"jsonPointer\": {\n          return <CodeIcon className={classes} />;\n        }\n        case \"jwt\": {\n          return <KeyIcon className={classes} />;\n        }\n        case \"semver\": {\n          return <DocumentTextIcon className={classes} />;\n        }\n        case \"color\": {\n          return <ColorSwatchIcon className={classes} />;\n        }\n        case \"creditcard\": {\n          switch (type.format.variant) {\n            case \"visa\": {\n              return <CreditCardIcon className={classes} />;\n            }\n            case \"mastercard\": {\n              return <CreditCardIcon className={classes} />;\n            }\n            case \"amex\": {\n              return <CreditCardIcon className={classes} />;\n            }\n            case \"discover\": {\n              return <CreditCardIcon className={classes} />;\n            }\n            case \"dinersclub\": {\n              return <CreditCardIcon className={classes} />;\n            }\n            default:\n              return <CreditCardIcon className={classes} />;\n          }\n        }\n      }\n    }\n  }\n\n  return <></>;\n};\n"
  },
  {
    "path": "app/components/json-schema-map.d.ts",
    "content": "declare module \"json-source-map\" {\n  export interface ParseOptions {\n    bigint?: boolean;\n  }\n\n  export type PointerProp = \"value\" | \"valueEnd\" | \"key\" | \"keyEnd\";\n\n  export interface Location {\n    line: number;\n    column: number;\n    pos: number;\n  }\n\n  export type Pointers = Record<string, Record<PointerProp, Location>>;\n\n  export interface ParseResult {\n    data: any;\n    pointers: Pointers;\n  }\n\n  export function parse(\n    source: string,\n    _reviver?: any,\n    options?: ParseOptions\n  ): ParseResult;\n\n  export interface StringifyOptions {\n    space?: string | number;\n    es6?: boolean;\n  }\n\n  export interface StringifyResult {\n    json: string;\n    pointers: Pointers;\n  }\n\n  export function stringify(\n    data: any,\n    _replacer?: any,\n    options?: string | number | StringifyOptions\n  ): StringifyResult;\n}\n"
  },
  {
    "path": "app/entry.client.tsx",
    "content": "import { hydrate } from \"react-dom\";\nimport { RemixBrowser } from \"remix\";\nimport { load } from \"fathom-client\";\n\nhydrate(<RemixBrowser />, document);\n\nload(\"ROBFNTET\", {\n  spa: \"history\",\n  excludedDomains: [\"localhost\"],\n  includedDomains: [\"jsonhero.io\"],\n});\n"
  },
  {
    "path": "app/entry.server.tsx",
    "content": "import { renderToString } from \"react-dom/server\";\nimport { RemixServer } from \"remix\";\nimport type { EntryContext } from \"remix\";\n\nexport default function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  remixContext: EntryContext\n) {\n  const markup = renderToString(\n    <RemixServer context={remixContext} url={request.url} />\n  );\n\n  responseHeaders.set(\"Content-Type\", \"text/html\");\n\n  return new Response(\"<!DOCTYPE html>\" + markup, {\n    status: responseStatusCode,\n    headers: responseHeaders\n  });\n}\n"
  },
  {
    "path": "app/entry.worker.ts",
    "content": "/// <reference lib=\"WebWorker\" />\nimport { JSONHeroSearch } from \"@jsonhero/fuzzy-json-search\";\nimport { inferType } from \"@jsonhero/json-infer-types\";\nimport { formatValue } from \"./utilities/formatter\";\n\ntype SearchWorker = {\n  searcher?: JSONHeroSearch;\n};\n\nexport type {};\ndeclare let self: DedicatedWorkerGlobalScope & SearchWorker;\n\ntype InitializeIndexEvent = {\n  type: \"initialize-index\";\n  payload: { json: unknown };\n};\n\ntype SearchEvent = {\n  type: \"search\";\n  payload: { query: string };\n};\n\ntype SearchWorkerEvent = InitializeIndexEvent | SearchEvent;\n\nself.onmessage = (e: MessageEvent<SearchWorkerEvent>) => {\n  const { type, payload } = e.data;\n\n  console.group(`SearchWorker: ${type}`);\n  console.log(payload);\n\n  switch (type) {\n    case \"initialize-index\": {\n      const { json } = payload;\n\n      self.searcher = new JSONHeroSearch(json, {\n        cacheSettings: { max: 100, enabled: true },\n        formatter: valueFormatter,\n      });\n      self.searcher.prepareIndex();\n\n      self.postMessage({ type: \"index-initialized\" });\n\n      break;\n    }\n    case \"search\": {\n      const { query } = payload;\n\n      if (!self.searcher) {\n        throw new Error(\"Search index not initialized\");\n      }\n\n      const start = performance.now();\n\n      const results = self.searcher.search(query);\n\n      const end = performance.now();\n\n      console.log(`Search took ${end - start}ms`);\n\n      console.log(\"results\", results);\n\n      self.postMessage({\n        type: \"search-results\",\n        payload: { results, query },\n      });\n    }\n  }\n\n  console.groupEnd();\n};\n\nfunction valueFormatter(value: unknown): string | undefined {\n  const inferredType = inferType(value);\n\n  return formatValue(inferredType);\n}\n"
  },
  {
    "path": "app/graphJSON.server.ts",
    "content": "export async function sendEvent(event: Record<string, any>): Promise<void> {\n  return;\n}\n\nfunction graphJsonReplacer(key: string, value: any): any {\n  if (key === \"api_key\") {\n    return undefined;\n  }\n\n  return value;\n}\n"
  },
  {
    "path": "app/hooks/useClickOutside.tsx",
    "content": "import { RefObject, useEffect, useRef } from \"react\";\n\nexport function useClickOutside(\n  elementRef: RefObject<HTMLElement>,\n  callback: (event: MouseEvent) => void\n) {\n  const callbackRef = useRef<(event: MouseEvent) => void>();\n  callbackRef.current = callback;\n\n  useEffect(() => {\n    const handleClickOutside = (e: MouseEvent) => {\n      if (!elementRef?.current || !callbackRef.current) {\n        return;\n      }\n\n      if (e.target instanceof Element) {\n        if (!elementRef.current.contains(e.target)) {\n          callbackRef.current(e);\n        }\n      }\n    };\n\n    document.addEventListener(\"click\", handleClickOutside, true);\n\n    return () => {\n      document.removeEventListener(\"click\", handleClickOutside, true);\n    };\n  }, [callbackRef, elementRef]);\n}\n"
  },
  {
    "path": "app/hooks/useIsMounted.tsx",
    "content": "import { useRef, useEffect, useCallback } from \"react\";\n\nexport function useIsMounted(): () => boolean {\n  const ref = useRef(false);\n\n  useEffect(() => {\n    ref.current = true;\n    return () => {\n      ref.current = false;\n    };\n  }, []);\n\n  return useCallback(() => ref.current, [ref]);\n}\n"
  },
  {
    "path": "app/hooks/useJson.tsx",
    "content": "import {\n  createContext,\n  Dispatch,\n  ReactNode,\n  SetStateAction,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport invariant from \"tiny-invariant\";\nimport { stableJson } from \"~/utilities/stableJson\";\n\ntype JsonContextType = [unknown, Dispatch<SetStateAction<unknown>>];\n\nconst JsonContext = createContext<JsonContextType | undefined>(undefined);\n\nexport function JsonProvider({\n  children,\n  initialJson,\n}: {\n  children: ReactNode;\n  initialJson: unknown;\n}) {\n  const stablizedJson = useMemo(() => stableJson(initialJson), [initialJson]);\n\n  const [json, setJson] = useState<unknown>(stablizedJson);\n\n  return (\n    <JsonContext.Provider value={[json, setJson]}>\n      {children}\n    </JsonContext.Provider>\n  );\n}\n\nexport function useJson(): JsonContextType {\n  const context = useContext(JsonContext);\n\n  invariant(context, \"useJson must be used within a JsonProvider\");\n\n  return context;\n}\n"
  },
  {
    "path": "app/hooks/useJsonColumnView.tsx",
    "content": "import { JSONHeroPath } from \"@jsonhero/path\";\nimport { pick } from \"lodash-es\";\nimport React, { useEffect, useRef } from \"react\";\nimport { createContext, ReactNode, useContext } from \"react\";\nimport invariant from \"tiny-invariant\";\nimport {\n  ColumnViewState,\n  ColumnViewAction,\n  useColumnView,\n  ColumnViewInstanceState,\n  ColumnViewAPI,\n} from \"~/useColumnView\";\nimport {\n  generateColumnViewNode,\n  calculateStablePath,\n  firstChildToDescendant,\n} from \"~/utilities/jsonColumnView\";\nimport { useJson } from \"./useJson\";\nimport { useJsonDoc } from \"./useJsonDoc\";\n\nexport type JsonColumnViewState = ColumnViewInstanceState;\nexport type JsonColumnViewAPI = ColumnViewAPI;\n\nconst JsonColumnViewStateContext = createContext<JsonColumnViewState>(\n  {} as JsonColumnViewState\n);\n\nconst JsonColumnViewAPIContext = createContext<JsonColumnViewAPI>(\n  {} as JsonColumnViewAPI\n);\n\nexport function JsonColumnViewProvider({ children }: { children: ReactNode }) {\n  const [json] = useJson();\n  const { doc, path: initialNodeId } = useJsonDoc();\n\n  const rootNode = React.useMemo(() => {\n    return generateColumnViewNode(json);\n  }, [json]);\n\n  const jsonReducer = React.useCallback(\n    (\n      state: ColumnViewState,\n      action: ColumnViewAction,\n      changes: ColumnViewState\n    ): ColumnViewState => {\n      if (action.type === \"MOVE_UP\" || action.type == \"MOVE_DOWN\") {\n        const { selectedNodeId } = state;\n        const { highlightedNodeId } = changes;\n\n        invariant(selectedNodeId, \"expected selectedNodeId\");\n        invariant(highlightedNodeId, \"expected highlightedNodeId\");\n\n        const calculatedPath = calculateStablePath(\n          selectedNodeId,\n          highlightedNodeId,\n          json\n        );\n\n        return {\n          ...changes,\n          selectedNodeId: calculatedPath,\n        };\n      }\n\n      if (\n        action.type === \"MOVE_TO_PARENT\" &&\n        action.source &&\n        action.source.altKey\n      ) {\n        const { selectedNodeId } = state;\n\n        return {\n          ...changes,\n          selectedNodeId,\n        };\n      }\n\n      if (action.type === \"MOVE_TO_CHILDREN\") {\n        const { selectedNodeId, highlightedNodeId } = state;\n\n        invariant(selectedNodeId, \"expected selectedNodeId\");\n        invariant(highlightedNodeId, \"expected highlightedNodeId\");\n\n        // If the previous highlightedNodeId is an ancestor of the previous selectedNodeId\n        if (isAncestorOf(highlightedNodeId, selectedNodeId)) {\n          // Get the next child of the highlightedNodeId in the path of selectedNodeId\n          // And make the highlightedNodeId that next child\n          // And keep the selectedNodeId unchanged\n          const highlightedPath = new JSONHeroPath(highlightedNodeId);\n          const selectedPath = new JSONHeroPath(selectedNodeId);\n\n          const childPath = firstChildToDescendant(\n            highlightedPath,\n            selectedPath\n          );\n\n          if (!childPath) {\n            return changes;\n          }\n\n          return {\n            ...changes,\n            highlightedNodeId: childPath,\n            selectedNodeId,\n          };\n        } else {\n          return changes;\n        }\n      }\n\n      return changes;\n    },\n    [json]\n  );\n\n  const { state, api } = useColumnView({\n    rootNode,\n    initialState: initialNodeId ?? \"$\",\n    stateReducer: jsonReducer,\n  });\n\n  const isStateRestored = useRef<boolean>(!!initialNodeId);\n\n  // This is restoring the state\n  useEffect(() => {\n    if (isStateRestored.current) {\n      return;\n    }\n\n    isStateRestored.current = true;\n\n    const storage = localStorage.getItem(doc.id);\n    if (storage == null) {\n      api.goToNextSibling();\n      return;\n    }\n\n    const restoredState = JSON.parse(storage) as ColumnViewInstanceState;\n    if (!restoredState.selectedNodeId) {\n      api.goToNextSibling();\n      return;\n    }\n\n    api.goToNodeId(restoredState.selectedNodeId, \"localStorage\");\n  }, [doc.id, isStateRestored.current, state, api]);\n\n  // This is setting the state\n  useEffect(() => {\n    if (doc == null) {\n      return;\n    }\n    if (!isStateRestored.current) {\n      return;\n    }\n    localStorage.setItem(\n      doc.id,\n      JSON.stringify(pick(state, \"selectedNodeId\", \"highlightedNodeId\"))\n    );\n  }, [\n    isStateRestored.current,\n    doc.id,\n    state.selectedNodeId,\n    state.highlightedNodeId,\n  ]);\n\n  return (\n    <JsonColumnViewAPIContext.Provider value={api}>\n      <JsonColumnViewStateContext.Provider value={state}>\n        {children}\n      </JsonColumnViewStateContext.Provider>\n    </JsonColumnViewAPIContext.Provider>\n  );\n}\n\nexport function useJsonColumnViewState(): JsonColumnViewState {\n  const context = useContext(JsonColumnViewStateContext);\n\n  invariant(\n    context,\n    \"useJsonColumnViewState must be used within a JsonColumnViewStateContext.Provider\"\n  );\n\n  return context;\n}\n\nexport function useJsonColumnViewAPI(): JsonColumnViewAPI {\n  const context = useContext(JsonColumnViewAPIContext);\n\n  invariant(\n    context,\n    \"useJsonColumnViewAPI must be used within a JsonColumnViewAPIContext.Provider\"\n  );\n\n  return context;\n}\n\nfunction isAncestorOf(ancestor: string, descendant: string) {\n  return ancestor != descendant && descendant.startsWith(ancestor);\n}\n"
  },
  {
    "path": "app/hooks/useJsonDoc.tsx",
    "content": "import { createContext, ReactNode, useContext } from \"react\";\nimport invariant from \"tiny-invariant\";\nimport { JSONDocument } from \"~/jsonDoc.server\";\n\ntype JsonDocType = {\n  doc: JSONDocument;\n  path?: string;\n  minimal?: boolean;\n};\n\nconst JsonDocContext = createContext<JsonDocType | undefined>(undefined);\n\nexport function JsonDocProvider({\n  children,\n  doc,\n  path,\n  minimal,\n}: {\n  children: ReactNode;\n  doc: JSONDocument;\n  path?: string;\n  minimal?: boolean;\n}) {\n  return (\n    <JsonDocContext.Provider value={{ doc, path, minimal }}>\n      {children}\n    </JsonDocContext.Provider>\n  );\n}\n\nexport function useJsonDoc(): JsonDocType {\n  const context = useContext(JsonDocContext);\n\n  invariant(context, \"useJsonDoc must be used within a JsonDocProvider\");\n\n  return context;\n}\n"
  },
  {
    "path": "app/hooks/useJsonSchema.tsx",
    "content": "import { Schema } from \"@jsonhero/json-schema-fns\";\nimport { inferSchema } from \"@jsonhero/schema-infer\";\nimport { createContext, ReactNode, useContext, useMemo } from \"react\";\nimport invariant from \"tiny-invariant\";\nimport { useJson } from \"./useJson\";\n\nconst JsonSchemaContext = createContext<Schema | undefined>(undefined);\n\nexport function JsonSchemaProvider({ children }: { children: ReactNode }) {\n  const [json] = useJson();\n\n  const jsonSchema = useMemo(\n    () => inferSchema(json).toJSONSchema({ includeSchema: true }),\n    [json]\n  );\n\n  return (\n    <JsonSchemaContext.Provider value={jsonSchema}>\n      {children}\n    </JsonSchemaContext.Provider>\n  );\n}\n\nexport function useJsonSchema(): Schema {\n  const context = useContext(JsonSchemaContext);\n\n  invariant(context, \"useJsonSchema must be used within a JsonSchemaProvider\");\n\n  return context;\n}\n"
  },
  {
    "path": "app/hooks/useJsonSearch.tsx",
    "content": "import { useJson } from \"./useJson\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useReducer,\n  useRef,\n} from \"react\";\n\nimport { SearchResult } from \"@jsonhero/fuzzy-json-search\";\n\nexport type InitializeIndexEvent = {\n  type: \"initialize-index\";\n  payload: { json: unknown };\n};\n\nexport type SearchEvent = {\n  type: \"search\";\n  payload: { query: string };\n};\n\nexport type SearchSendWorkerEvent = InitializeIndexEvent | SearchEvent;\n\nexport type IndexInitializedEvent = {\n  type: \"index-initialized\";\n};\n\nexport type SearchResultsEvent = {\n  type: \"search-results\";\n  payload: { results: Array<SearchResult<string>>; query: string };\n};\n\nexport type SearchReceiveWorkerEvent =\n  | IndexInitializedEvent\n  | SearchResultsEvent;\n\nexport type JsonSearchApi = {\n  search: (query: string) => void;\n  reset: () => void;\n};\n\nconst JsonSearchStateContext = createContext<JsonSearchState>(\n  {} as JsonSearchState\n);\n\nconst JsonSearchApiContext = createContext<JsonSearchApi>({} as JsonSearchApi);\n\nexport type JsonSearchState = {\n  status: \"initializing\" | \"idle\" | \"searching\";\n  query?: string;\n  results?: Array<SearchResult<string>>;\n};\n\ntype SearchAction = {\n  type: \"search\";\n  payload: { query: string };\n};\n\ntype ResetAction = {\n  type: \"reset\";\n};\n\ntype JsonSearchAction = SearchReceiveWorkerEvent | SearchAction | ResetAction;\n\nfunction reducer(\n  state: JsonSearchState,\n  action: JsonSearchAction\n): JsonSearchState {\n  switch (state.status) {\n    case \"initializing\": {\n      if (action.type === \"index-initialized\") {\n        return {\n          ...state,\n          status: \"idle\",\n          results: undefined,\n        };\n      }\n\n      return state;\n    }\n    case \"idle\": {\n      if (action.type === \"reset\") {\n        return {\n          ...state,\n          query: undefined,\n          results: undefined,\n        };\n      }\n\n      if (action.type === \"search\") {\n        return {\n          ...state,\n          status: \"searching\",\n          query: action.payload.query,\n        };\n      }\n\n      return state;\n    }\n    case \"searching\": {\n      if (action.type === \"reset\") {\n        return {\n          ...state,\n          status: \"idle\",\n          query: undefined,\n          results: undefined,\n        };\n      }\n\n      if (\n        action.type === \"search-results\" &&\n        state.query === action.payload.query\n      ) {\n        return {\n          ...state,\n          status: \"idle\",\n          results: action.payload.results,\n        };\n      }\n\n      return state;\n    }\n  }\n}\n\nlet lastAction: any | undefined;\n\nfunction wrapReducer<S, A extends { type: string }>(\n  name: string,\n  reducer: React.Reducer<S, A>\n): React.Reducer<S, A> {\n  return (state, action) => {\n    const next = reducer(state, action);\n\n    if (process.env.NODE_ENV !== \"production\") {\n      if (!lastAction) {\n        console.groupCollapsed(\n          `%cAction: %c${\n            name + \" \" + action.type\n          } %cat ${getCurrentTimeFormatted()}`,\n          \"color: lightgreen; font-weight: bold;\",\n          \"color: white; font-weight: bold;\",\n          \"color: lightblue; font-weight: lighter;\"\n        );\n        console.log(\n          \"%cPrevious State:\",\n          \"color: #9E9E9E; font-weight: 700;\",\n          state\n        );\n        console.log(\"%cAction:\", \"color: #00A7F7; font-weight: 700;\", action);\n        console.log(\"%cNext State:\", \"color: #47B04B; font-weight: 700;\", next);\n        console.groupEnd();\n        lastAction = action;\n      } else {\n        lastAction = undefined;\n      }\n    }\n\n    return next;\n  };\n}\n\nconst getCurrentTimeFormatted = () => {\n  const currentTime = new Date();\n  const hours = currentTime.getHours();\n  const minutes = currentTime.getMinutes();\n  const seconds = currentTime.getSeconds();\n  const milliseconds = currentTime.getMilliseconds();\n  return `${hours}:${minutes}:${seconds}.${milliseconds}`;\n};\n\nexport function JsonSearchProvider({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const [json] = useJson();\n\n  const [state, dispatch] = useReducer<\n    React.Reducer<JsonSearchState, JsonSearchAction>\n  >(wrapReducer(\"jsonSearch\", reducer), { status: \"initializing\" });\n\n  const search = useCallback(\n    (query: string) => {\n      dispatch({ type: \"search\", payload: { query } });\n    },\n    [dispatch]\n  );\n\n  const reset = useCallback(() => {\n    dispatch({ type: \"reset\" });\n  }, [dispatch]);\n\n  const handleWorkerMessage = useCallback(\n    (e: MessageEvent<SearchReceiveWorkerEvent>) => dispatch(e.data),\n    [dispatch]\n  );\n\n  const workerRef = useRef<Worker | null>();\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || typeof window.Worker === \"undefined\") {\n      return;\n    }\n\n    if (workerRef.current) {\n      return;\n    }\n\n    const worker = new Worker(\"/entry.worker.js\");\n    worker.onmessage = handleWorkerMessage;\n\n    workerRef.current = worker;\n\n    workerRef.current.postMessage({\n      type: \"initialize-index\",\n      payload: {\n        json,\n      },\n    });\n  }, [json, workerRef.current]);\n\n  useEffect(() => {\n    if (state.status !== \"searching\") {\n      return;\n    }\n\n    workerRef.current?.postMessage({\n      type: \"search\",\n      payload: { query: state.query },\n    });\n  }, [state.status, workerRef.current]);\n\n  return (\n    <JsonSearchStateContext.Provider value={state}>\n      <JsonSearchApiContext.Provider value={{ search, reset }}>\n        {children}\n      </JsonSearchApiContext.Provider>\n    </JsonSearchStateContext.Provider>\n  );\n}\n\nexport function useJsonSearchState(): JsonSearchState {\n  return useContext(JsonSearchStateContext);\n}\n\nexport function useJsonSearchApi(): JsonSearchApi {\n  return useContext(JsonSearchApiContext);\n}\n"
  },
  {
    "path": "app/hooks/useJsonTree.tsx",
    "content": "import { useJson } from \"./useJson\";\nimport { inferType, JSONValueType } from \"@jsonhero/json-infer-types\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\nimport { IconComponent } from \"~/useColumnView\";\nimport { formatValue } from \"~/utilities/formatter\";\nimport { iconForType } from \"~/utilities/icons\";\nimport {\n  createContext,\n  ReactNode,\n  useCallback,\n  useContext,\n  useMemo,\n  useRef,\n} from \"react\";\nimport { useVirtualTree, UseVirtualTreeInstance } from \"./useVirtualTree\";\nimport invariant from \"tiny-invariant\";\nimport { useJsonDoc } from \"./useJsonDoc\";\n\nconst initialRect = { width: 800, height: 600 };\n\nexport type JsonTreeOptions = {\n  overscan?: number;\n};\n\nexport type UseJsonTreeInstance = {\n  tree: UseVirtualTreeInstance<JsonTreeViewNode>;\n  parentRef: React.RefObject<HTMLDivElement>;\n};\n\nexport type JsonTreeViewType = UseJsonTreeInstance;\n\nconst JsonTreeViewContext = createContext<JsonTreeViewType>(\n  {} as JsonTreeViewType\n);\n\nexport function JsonTreeViewProvider({\n  children,\n  ...options\n}: { children: ReactNode } & JsonTreeOptions) {\n  const instance = useJsonTree(options);\n\n  return (\n    <JsonTreeViewContext.Provider value={instance}>\n      {children}\n    </JsonTreeViewContext.Provider>\n  );\n}\n\nexport function useJsonTree(options: JsonTreeOptions): UseJsonTreeInstance {\n  const parentRef = useRef<HTMLDivElement>(null);\n\n  const { doc } = useJsonDoc();\n  const [json] = useJson();\n  const jsonNodes = useMemo(() => {\n    return generateTreeViewNodes(json);\n  }, [json]);\n\n  const tree = useVirtualTree({\n    id: doc.id,\n    nodes: jsonNodes,\n    parentRef,\n    estimateSize: useCallback((index) => 32, []),\n    initialRect,\n    overscan: options.overscan,\n    persistState: true,\n  });\n\n  return { tree, parentRef };\n}\n\nexport function useJsonTreeViewContext(): JsonTreeViewType {\n  const context = useContext(JsonTreeViewContext);\n\n  invariant(\n    context,\n    \"useJsonTreeViewContext must be used within a JsonTreeViewContext.Provider\"\n  );\n\n  return context;\n}\n\nexport type JsonTreeViewNode = {\n  id: string;\n  name: string;\n  title: string;\n  subtitle?: string;\n  longTitle?: string;\n  icon?: IconComponent;\n  children?: Array<JsonTreeViewNode>;\n};\n\nexport function generateTreeViewNodes(json: unknown): Array<JsonTreeViewNode> {\n  const info = inferType(json);\n  const path = new JSONHeroPath(\"$\");\n\n  return generateChildren(info, path) ?? [];\n}\n\nfunction generateChildren(\n  info: JSONValueType,\n  path: JSONHeroPath\n): Array<JsonTreeViewNode> | undefined {\n  if (info.name === \"array\") {\n    return info.value.map((item, index) => {\n      const itemInfo = inferType(item);\n      const itemPath = path.child(index.toString());\n\n      return {\n        id: itemPath.toString(),\n        name: index.toString(),\n        title: index.toString(),\n        longTitle: `Index ${index.toString()}`,\n        subtitle: formatValue(itemInfo),\n        icon: iconForType(itemInfo),\n        children: generateChildren(itemInfo, itemPath),\n      };\n    });\n  }\n\n  if (info.name === \"object\") {\n    return Object.entries(info.value).map(([key, value]) => {\n      const cleanKey = key.replace(/\\./g, \"\\\\.\");\n      const itemInfo = inferType(value);\n      const itemPath = path.child(cleanKey);\n      return {\n        id: itemPath.toString(),\n        name: key,\n        title: key,\n        subtitle: formatValue(itemInfo),\n        icon: iconForType(itemInfo),\n        children: generateChildren(itemInfo, itemPath),\n      };\n    });\n  }\n}\n"
  },
  {
    "path": "app/hooks/useLoadWhenOnline.tsx",
    "content": "import { useEffect, useRef } from \"react\";\n\nexport function useLoadWhenOnline(callback: () => void, deps: unknown[] = []) {\n    const callbackRef = useRef <() => void>(callback);\n\n    useEffect(() => {\n        callbackRef.current = callback;\n    });\n\n    useEffect(() => {\n        const cb = () => callbackRef.current();\n\n        if (window.navigator.onLine) {\n            cb();\n            return;\n        }\n\n        window.addEventListener(\"online\", cb);\n\n        return () => {\n            window.removeEventListener(\"online\", cb);\n        };\n    }, [...deps]);\n}\n"
  },
  {
    "path": "app/hooks/useMemoCompare.ts",
    "content": "import { useEffect, useRef } from \"react\";\n\nexport function useMemoCompare<T>(\n  next: T | null | undefined,\n  compare: (\n    previous: T | null | undefined,\n    next: T | null | undefined\n  ) => boolean\n) {\n  // Ref for storing previous value\n  const previousRef = useRef<T | null | undefined>();\n  const previous = previousRef.current;\n  // Pass previous and next value to compare function\n  // to determine whether to consider them equal.\n  const isEqual = compare(previous, next);\n  // If not equal update previousRef to next value.\n  // We only update if not equal so that this hook continues to return\n  // the same old value if compare keeps returning true.\n  useEffect(() => {\n    if (!isEqual) {\n      previousRef.current = next;\n    }\n  });\n  // Finally, if equal then return the previous value\n  return isEqual ? previous : next;\n}\n"
  },
  {
    "path": "app/hooks/useOnScreen.tsx",
    "content": "import { useEffect, useState, useRef, RefObject } from \"react\";\n\nexport function useOnScreen(ref: RefObject<HTMLElement>) {\n  const observerRef = useRef<IntersectionObserver | null>(null);\n  const [isOnScreen, setIsOnScreen] = useState(false);\n\n  useEffect(() => {\n    observerRef.current = new IntersectionObserver(([entry]) =>\n      setIsOnScreen(entry.isIntersecting)\n    );\n  }, []);\n\n  useEffect(() => {\n    if (observerRef.current == null || ref.current == null) return;\n    observerRef.current.observe(ref.current);\n\n    return () => {\n      if (observerRef.current == null) return;\n      observerRef.current.disconnect();\n    };\n  }, [ref]);\n\n  return isOnScreen;\n}\n"
  },
  {
    "path": "app/hooks/useRelatedPaths.ts",
    "content": "import { useMemo, useRef } from \"react\";\nimport { getRelatedPathsAtPath } from \"~/utilities/relatedValues\";\nimport { useJson } from \"./useJson\";\nimport { useJsonColumnViewState } from \"./useJsonColumnView\";\n\nexport function useRelatedPaths(): string[] {\n  const cache = useRef(new Map<string, Array<string>>());\n  const { selectedNodeId } = useJsonColumnViewState();\n  const [json] = useJson();\n\n  return useMemo(() => {\n    if (!selectedNodeId) return [];\n\n    //check cache\n    const cachedPaths = cache.current.get(selectedNodeId);\n    if (cachedPaths) {\n      return cachedPaths;\n    }\n\n    //fetch result\n    let paths = getRelatedPathsAtPath(selectedNodeId, json);\n\n    //cache\n    for (let index = 0; index < paths.length; index++) {\n      const path = paths[index];\n      cache.current.set(path, paths);\n    }\n\n    return paths;\n  }, [selectedNodeId, json]);\n}\n"
  },
  {
    "path": "app/hooks/useSelectedInfo.tsx",
    "content": "import { inferType, JSONValueType } from \"@jsonhero/json-infer-types\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\nimport { useMemo } from \"react\";\nimport { useJson } from \"./useJson\";\nimport { useJsonColumnViewState } from \"./useJsonColumnView\";\n\nexport function useSelectedInfo(): JSONValueType | undefined {\n  const { selectedNodeId } = useJsonColumnViewState();\n\n  if (!selectedNodeId) {\n    return;\n  }\n\n  const [json] = useJson();\n\n  return useMemo(() => {\n    const heroPath = new JSONHeroPath(selectedNodeId);\n    const selectedJson = heroPath.first(json);\n    return inferType(selectedJson);\n  }, [json, selectedNodeId]);\n}\n"
  },
  {
    "path": "app/hooks/useVirtualTree.ts",
    "content": "import pick from \"lodash-es/pick\";\nimport React, {\n  useReducer,\n  Reducer,\n  useCallback,\n  Dispatch,\n  useEffect,\n  useRef,\n} from \"react\";\nimport { useVirtual, VirtualItem } from \"react-virtual\";\n\ntype UseVirtualOptions<R> = Parameters<typeof useVirtual>[0];\n\nexport type UseVirtualTreeOptions<\n  T extends { id: string; children?: T[] },\n  R\n> = {\n  id: string;\n  persistState?: boolean;\n  nodes: T[];\n} & Omit<UseVirtualOptions<R>, \"size\">;\n\nexport type VirtualNode<T> = {\n  node: T;\n  size: number; // This is the same as virtualItem.size\n  start: number; // This is the same as virtualItem.start\n  virtualItem: VirtualItem;\n  depth: number;\n  getItemProps: () => React.HTMLAttributes<HTMLElement>;\n  isCollapsed?: boolean;\n};\n\nexport type UseVirtualTreeInstance<T> = {\n  nodes: VirtualNode<T>[];\n  focusedNodeId: string | null;\n  totalSize: number;\n  toggleNode: (id: string, source?: KeyboardEvent | MouseEvent) => void;\n  focusNode: (id: string) => void;\n  focusFirst: () => void;\n  blur: () => void;\n  scrollToNode: (id: string) => void;\n  getTreeProps: () => React.HTMLAttributes<HTMLElement>;\n};\n\ntype TreeNodeItem<T extends { id: string; children?: T[] }> = {\n  id: string;\n  depth: number;\n  node: T;\n  pos: number;\n  size: number;\n  isCollapsed: boolean;\n};\n\ntype TreeState<T extends { id: string; children?: T[] }> = {\n  nodes: T[];\n  items: TreeNodeItem<T>[];\n  collapsedState: Record<string, boolean>;\n  focusedNodeId: string | null;\n};\n\ntype ToggleNodeAction = {\n  type: \"TOGGLE_NODE\";\n  id: string;\n  source?: KeyboardEvent | MouseEvent;\n};\n\ntype FocusNodeAction = {\n  type: \"FOCUS_NODE\";\n  id: string;\n};\n\ntype MoveNodeAction = {\n  type: \"MOVE_DOWN\" | \"MOVE_UP\" | \"MOVE_TO_TOP\" | \"MOVE_TO_BOTTOM\";\n  source: KeyboardEvent | MouseEvent;\n};\n\ntype MoveRightAction = {\n  type: \"MOVE_RIGHT\";\n  source: KeyboardEvent | MouseEvent;\n};\n\ntype MoveLeftAction = {\n  type: \"MOVE_LEFT\";\n  source: KeyboardEvent | MouseEvent;\n};\n\ntype FocusFirstAction = {\n  type: \"FOCUS_FIRST\";\n};\n\ntype RestoreStateAction = {\n  type: \"RESTORE_STATE\";\n  restoredState: { collapsedState: Record<string, boolean> };\n};\n\ntype ExpandAllOnPathAction = {\n  type: \"EXPAND_ALL_ON_PATH\";\n  path: string[];\n};\n\ntype BlurAction = {\n  type: \"BLUR\";\n};\n\ntype CollapseAllNodesAction = {\n  type: \"COLLAPSE_ALL_NODES\"\n};\n\ntype TreeAction =\n  | ToggleNodeAction\n  | MoveNodeAction\n  | FocusNodeAction\n  | FocusFirstAction\n  | MoveRightAction\n  | MoveLeftAction\n  | RestoreStateAction\n  | ExpandAllOnPathAction\n  | BlurAction\n  | CollapseAllNodesAction;\n\nfunction expandNode<T extends { id: string; children?: T[] }>(\n  state: TreeState<T>,\n  id: string\n): TreeState<T> {\n  const collapsedState = {\n    ...state.collapsedState,\n    [id]: false,\n  };\n\n  return {\n    ...state,\n    collapsedState,\n    items: createNodeItems(state.nodes, 0, collapsedState),\n    focusedNodeId: id,\n  };\n}\n\nfunction collapseNode<T extends { id: string; children?: T[] }>(\n  state: TreeState<T>,\n  id: string\n): TreeState<T> {\n  const collapsedState = {\n    ...state.collapsedState,\n    [id]: true,\n  };\n\n  return {\n    ...state,\n    collapsedState,\n    items: createNodeItems(state.nodes, 0, collapsedState),\n    focusedNodeId: id,\n  };\n}\nfunction toggleAllChildren<T extends { id: string; children?: T[] }>(\n  state: TreeState<T>,\n  id: string\n): TreeState<T> {\n  const item = state.items.find(({ id: nodeId }) => nodeId === id);\n\n  if (!item) {\n    return state;\n  }\n\n  if (!item.node.children || item.node.children.length === 0) {\n    return state;\n  }\n\n  const allCollapsed = item.node.children.every(\n    (child) => state.collapsedState[child.id]\n  );\n\n  if (allCollapsed) {\n    const collapsedState = item.node.children.reduce(\n      (acc, child) => ({\n        ...acc,\n        [child.id]: false,\n      }),\n      state.collapsedState\n    );\n\n    return {\n      ...state,\n      collapsedState,\n      items: createNodeItems(state.nodes, 0, collapsedState),\n      focusedNodeId: id,\n    };\n  }\n\n  const collapsedState = item.node.children.reduce(\n    (acc, child) => ({\n      ...acc,\n      [child.id]: true,\n    }),\n    state.collapsedState\n  );\n\n  return {\n    ...state,\n    collapsedState,\n    items: createNodeItems(state.nodes, 0, collapsedState),\n    focusedNodeId: id,\n  };\n}\n\nexport function useVirtualTree<T extends { id: string; children?: T[] }, R>(\n  options: UseVirtualTreeOptions<T, R>\n): UseVirtualTreeInstance<T> {\n  const reducer = useCallback<Reducer<TreeState<T>, TreeAction>>(\n    (state, action) => {\n      switch (action.type) {\n        case \"BLUR\": {\n          return {\n            ...state,\n            focusedNodeId: null,\n          };\n        }\n        case \"TOGGLE_NODE\": {\n          const isCollapsed = state.collapsedState[action.id];\n\n          if (isCollapsed) {\n            return expandNode<T>(state, action.id);\n          } else {\n            if (\n              action.source &&\n              (action.source.shiftKey || action.source.altKey)\n            ) {\n              return toggleAllChildren<T>(state, action.id);\n            } else {\n              return collapseNode<T>(state, action.id);\n            }\n          }\n        }\n        case \"COLLAPSE_ALL_NODES\": {\n          // Reduce from the right, so that the\n          // focusedNodeId is set to the top-level node.\n          return state.items.reduceRight(\n            (nextState, item) => collapseNode<T>(nextState, item.id),\n            state\n          );\n        }\n        case \"FOCUS_NODE\": {\n          const itemIndex = state.items.findIndex(({ id }) => id === action.id);\n\n          if (itemIndex === -1) {\n            const node = findNodeInTreeById(state.nodes, action.id);\n\n            if (!node) {\n              return state;\n            }\n\n            const path = calculatePathToNode(state.nodes, node) ?? [];\n\n            const collapsedState = path.reduce(\n              (acc, id) => ({\n                ...acc,\n                [id]: false,\n              }),\n              state.collapsedState\n            );\n\n            return {\n              ...state,\n              collapsedState,\n              items: createNodeItems(state.nodes, 0, collapsedState),\n              focusedNodeId: action.id,\n            };\n          }\n\n          return {\n            ...state,\n            focusedNodeId: action.id,\n          };\n        }\n        case \"FOCUS_FIRST\":\n        case \"MOVE_TO_TOP\": {\n          const nextItem = state.items[0];\n\n          if (!nextItem) {\n            return state;\n          }\n\n          return {\n            ...state,\n            focusedNodeId: nextItem.id,\n          };\n        }\n        case \"MOVE_TO_BOTTOM\": {\n          const nextItem = state.items[state.items.length - 1];\n\n          if (!nextItem) {\n            return state;\n          }\n\n          return {\n            ...state,\n            focusedNodeId: nextItem.id,\n          };\n        }\n        case \"MOVE_DOWN\": {\n          if (!state.focusedNodeId) {\n            const nextItem = state.items[0];\n\n            if (!nextItem) {\n              return state;\n            }\n\n            return {\n              ...state,\n              focusedNodeId: nextItem.id,\n            };\n          }\n\n          const focusedNodeIdIndex = state.items.findIndex(\n            (item) => item.id === state.focusedNodeId\n          );\n\n          if (focusedNodeIdIndex === -1) {\n            return state;\n          }\n\n          if (state.items.length <= focusedNodeIdIndex + 1) {\n            return state;\n          }\n\n          const nextItem = state.items[focusedNodeIdIndex + 1];\n\n          return {\n            ...state,\n            focusedNodeId: nextItem.id,\n          };\n        }\n        case \"MOVE_UP\": {\n          const focusedNodeIdIndex = state.items.findIndex(\n            (item) => item.id === state.focusedNodeId\n          );\n\n          if (focusedNodeIdIndex === -1) {\n            return state;\n          }\n\n          if (focusedNodeIdIndex === 0) {\n            return state;\n          }\n\n          const nextItem = state.items[focusedNodeIdIndex - 1];\n\n          return {\n            ...state,\n            focusedNodeId: nextItem.id,\n          };\n        }\n        case \"MOVE_RIGHT\": {\n          if (!state.focusedNodeId) {\n            return state;\n          }\n\n          const isCollapsed = state.collapsedState[state.focusedNodeId];\n\n          if (isCollapsed) {\n            return expandNode<T>(state, state.focusedNodeId);\n          }\n\n          if (\n            action.source &&\n            (action.source.shiftKey || action.source.altKey)\n          ) {\n            return toggleAllChildren<T>(state, state.focusedNodeId);\n          }\n\n          const nodeIndex = state.items.findIndex(\n            (item) => item.id === state.focusedNodeId\n          );\n\n          if (nodeIndex === -1) {\n            return state;\n          }\n\n          if (state.items.length <= nodeIndex + 1) {\n            return state;\n          }\n\n          const nextItem = state.items[nodeIndex + 1];\n\n          return {\n            ...state,\n            focusedNodeId: nextItem.id,\n          };\n        }\n        case \"MOVE_LEFT\": {\n          if (!state.focusedNodeId) {\n            return state;\n          }\n\n          const item = state.items.find(\n            (item) => item.id === state.focusedNodeId\n          );\n\n          if (!item) {\n            return state;\n          }\n\n          const hasChildren =\n            item.node.children && item.node.children.length > 0;\n          const isCollapsed = state.collapsedState[state.focusedNodeId];\n\n          if (hasChildren && !isCollapsed) {\n            if (\n              action.source &&\n              (action.source.shiftKey || action.source.altKey)\n            ) {\n              return toggleAllChildren<T>(state, state.focusedNodeId);\n            } else {\n              return collapseNode<T>(state, state.focusedNodeId);\n            }\n          }\n\n          if (!hasChildren || isCollapsed) {\n            // Try to go to the parent node\n            const parentNodeIndex = state.items.findIndex(\n              (item) =>\n                item.node.children &&\n                item.node.children\n                  .map((child) => child.id)\n                  .includes(state.focusedNodeId!)\n            );\n\n            if (parentNodeIndex === -1) {\n              return state;\n            }\n\n            const nextItem = state.items[parentNodeIndex];\n\n            return {\n              ...state,\n              focusedNodeId: nextItem.id,\n            };\n          }\n\n          return state;\n        }\n        case \"RESTORE_STATE\": {\n          const nextState = {\n            ...state,\n            ...action.restoredState,\n          };\n\n          return {\n            ...nextState,\n            items: createNodeItems(\n              nextState.nodes,\n              0,\n              nextState.collapsedState\n            ),\n          };\n        }\n        default:\n          return state;\n      }\n    },\n    []\n  );\n\n  const initializer = useCallback(\n    ({ nodes }: { nodes: T[] }) => {\n      return {\n        nodes,\n        items: createNodeItems(nodes),\n        collapsedState: {},\n        focusedNodeId: null,\n      };\n    },\n    [options.persistState, options.id]\n  );\n\n  const [state, dispatch] = useReducer<\n    Reducer<TreeState<T>, TreeAction>,\n    { nodes: T[] }\n  >(\n    reducer,\n    {\n      nodes: options.nodes,\n    },\n    initializer\n  );\n\n  const isStateRestored = useRef<boolean>(false);\n\n  // This is setting the state\n  useEffect(() => {\n    if (!isStateRestored.current) {\n      return;\n    }\n\n    if (options.persistState) {\n      localStorage.setItem(\n        `${options.id}-virtual-tree-state`,\n        JSON.stringify(pick(state, \"collapsedState\"))\n      );\n    }\n  }, [\n    state.collapsedState,\n    options.id,\n    options.persistState,\n    isStateRestored.current,\n  ]);\n\n  // This is restoring the state\n  useEffect(() => {\n    if (!options.persistState) {\n      return;\n    }\n\n    if (isStateRestored.current) {\n      return;\n    }\n\n    isStateRestored.current = true;\n\n    const savedState = localStorage.getItem(`${options.id}-virtual-tree-state`);\n\n    if (savedState) {\n      const restoredState = JSON.parse(savedState) as {\n        collapsedState: Record<string, boolean>;\n      };\n\n      dispatch({\n        type: \"RESTORE_STATE\",\n        restoredState,\n      });\n    }\n  }, [options.persistState, options.id, dispatch, isStateRestored.current]);\n\n  const rowVirtualizer = useVirtual({\n    size: state.items.length,\n    parentRef: options.parentRef,\n    estimateSize: options.estimateSize,\n    overscan: options.overscan,\n    initialRect: options.initialRect,\n    useObserver: options.useObserver,\n  });\n\n  const allVirtualNodes = rowVirtualizer.virtualItems.map((virtualItem) => {\n    const treeItem = state.items[virtualItem.index];\n\n    return {\n      node: treeItem.node,\n      depth: treeItem.depth,\n      size: virtualItem.size,\n      start: virtualItem.start,\n      virtualItem,\n      getItemProps: createItemProps(treeItem, virtualItem, state, dispatch),\n      isCollapsed: treeItem.isCollapsed,\n    };\n  });\n\n  const toggleNode = useCallback(\n    (id: string, source?: KeyboardEvent | MouseEvent) => {\n      dispatch({ type: \"TOGGLE_NODE\", id, source });\n    },\n    [dispatch]\n  );\n\n  const focusNode = useCallback(\n    (id: string) => {\n      dispatch({ type: \"FOCUS_NODE\", id });\n    },\n    [dispatch]\n  );\n\n  const focusFirst = useCallback(\n    () => dispatch({ type: \"FOCUS_FIRST\" }),\n    [dispatch]\n  );\n\n  const blur = useCallback(() => dispatch({ type: \"BLUR\" }), [dispatch]);\n\n  // TODO: have this work with collapsed nodes\n  const scrollToNode = useCallback(\n    (id: string) => {\n      const itemIndex = state.items.findIndex((item) => item.id === id);\n\n      if (itemIndex !== -1) {\n        rowVirtualizer.scrollToIndex(itemIndex, { align: \"auto\" });\n      }\n    },\n    [state.items, rowVirtualizer.scrollToIndex, dispatch]\n  );\n\n  useEffect(() => {\n    if (state.focusedNodeId) {\n      scrollToNode(state.focusedNodeId);\n    }\n  }, [state.focusedNodeId, scrollToNode]);\n\n  return {\n    nodes: allVirtualNodes,\n    totalSize: rowVirtualizer.totalSize,\n    toggleNode,\n    focusNode,\n    focusFirst,\n    blur,\n    focusedNodeId: state.focusedNodeId,\n    getTreeProps: useCallback(createTreeProps(dispatch), [dispatch]),\n    scrollToNode,\n  };\n}\n\nfunction createNodeItems<T extends { id: string; children?: T[] }>(\n  nodes: T[],\n  depth = 0,\n  collapsedState: Record<string, boolean> = {}\n): TreeNodeItem<T>[] {\n  return nodes.flatMap((node, index) => {\n    const children = node.children\n      ? collapsedState[node.id]\n        ? []\n        : createNodeItems(node.children, depth + 1, collapsedState)\n      : [];\n    return [\n      {\n        id: node.id,\n        depth,\n        node,\n        pos: index + 1,\n        size: nodes.length,\n        isCollapsed: !!collapsedState[node.id],\n      },\n      ...children,\n    ];\n  });\n}\n\nfunction createTreeProps<T extends { id: string; children?: T[] }>(\n  dispatch: Dispatch<TreeAction>\n): () => React.HTMLAttributes<HTMLElement> {\n  return () => ({\n    role: \"tree\",\n    tabIndex: -1,\n    onKeyDown: (e) => {\n      if (e.defaultPrevented) {\n        return; // Do nothing if the event was already processed\n      }\n\n      switch (e.key) {\n        case \"Home\": {\n          dispatch({ type: \"MOVE_TO_TOP\", source: e.nativeEvent });\n          e.preventDefault();\n          break;\n        }\n        case \"End\": {\n          dispatch({ type: \"MOVE_TO_BOTTOM\", source: e.nativeEvent });\n          e.preventDefault();\n          break;\n        }\n        case \"Down\":\n        case \"ArrowDown\": {\n          dispatch({ type: \"MOVE_DOWN\", source: e.nativeEvent });\n          e.preventDefault();\n          break;\n        }\n        case \"Up\":\n        case \"ArrowUp\": {\n          dispatch({ type: \"MOVE_UP\", source: e.nativeEvent });\n          e.preventDefault();\n          break;\n        }\n        case \"Left\":\n        case \"ArrowLeft\": {\n          if (e.altKey) {\n            dispatch({ type: \"COLLAPSE_ALL_NODES\" });\n          } else {\n            dispatch({\n              type: \"MOVE_LEFT\",\n              source: e.nativeEvent,\n            });\n          }\n          e.preventDefault();\n\n          break;\n        }\n        case \"Right\":\n        case \"ArrowRight\": {\n          dispatch({\n            type: \"MOVE_RIGHT\",\n            source: e.nativeEvent,\n          });\n          e.preventDefault();\n\n          break;\n        }\n      }\n    },\n  });\n}\n\nfunction createItemProps<T extends { id: string; children?: T[] }>(\n  item: TreeNodeItem<T>,\n  virtualItem: VirtualItem,\n  state: TreeState<T>,\n  dispatch: Dispatch<TreeAction>\n): () => React.HTMLAttributes<HTMLElement> {\n  const { depth, pos, size, node, isCollapsed } = item;\n\n  return () => ({\n    \"aria-expanded\": node.children && node.children.length > 0 && !isCollapsed,\n    \"aria-level\": depth + 1,\n    \"aria-posinset\": pos,\n    \"aria-setsize\": size,\n    role: \"treeitem\",\n    tabIndex: node.id === state.focusedNodeId ? -1 : undefined,\n    onClick: (e) => {\n      if (e.defaultPrevented) {\n        return; // Do nothing if the event was already processed\n      }\n\n      if (node.id !== state.focusedNodeId) {\n        dispatch({ type: \"FOCUS_NODE\", id: node.id });\n      }\n    },\n  });\n}\n\n// Finds the node in the list of nodes or recursively in the children\nfunction findNodeInTreeById<T extends { id: string; children?: T[] }>(\n  nodes: T[],\n  id: string\n): T | undefined {\n  const node = nodes.find((node) => node.id === id);\n\n  if (node) {\n    return node;\n  }\n\n  for (const node of nodes) {\n    const foundNode = findNodeInTreeById(node.children || [], id);\n\n    if (foundNode) {\n      return foundNode;\n    }\n  }\n\n  return;\n}\n\nfunction calculatePathToNode<T extends { id: string; children?: T[] }>(\n  nodes: T[],\n  searchNode: T,\n  path: string[] = []\n): string[] | undefined {\n  const nodeIndex = nodes.findIndex((node) => node.id === searchNode.id);\n\n  if (nodeIndex !== -1) {\n    return [...path, searchNode.id];\n  }\n\n  for (const node of nodes) {\n    if (!node.children) {\n      continue;\n    }\n\n    const foundPath = calculatePathToNode(node.children || [], searchNode, [\n      ...path,\n      node.id,\n    ]);\n\n    if (foundPath && foundPath.length > path.length) {\n      return foundPath;\n    }\n  }\n\n  return;\n}\n"
  },
  {
    "path": "app/jsonDoc.server.ts",
    "content": "import { customRandom } from \"nanoid\";\nimport safeFetch from \"./utilities/safeFetch\";\nimport createFromRawXml from \"./utilities/xml/createFromRawXml\";\nimport isXML from \"./utilities/xml/isXML\";\n\ntype BaseJsonDocument = {\n  id: string;\n  title: string;\n  readOnly: boolean;\n};\n\nexport type RawJsonDocument = BaseJsonDocument & {\n  type: \"raw\";\n  contents: string;\n};\n\nexport type UrlJsonDocument = BaseJsonDocument & {\n  type: \"url\";\n  url: string;\n};\n\nexport type CreateJsonOptions = {\n  ttl?: number;\n  readOnly?: boolean;\n  injest?: boolean;\n  metadata?: any;\n};\n\nexport type JSONDocument = RawJsonDocument | UrlJsonDocument;\n\nexport async function createFromUrlOrRawJson(\n  urlOrJson: string,\n  title?: string\n): Promise<JSONDocument | undefined> {\n  if (isUrl(urlOrJson)) {\n    return createFromUrl(new URL(urlOrJson), title);\n  }\n\n  if (isJSON(urlOrJson)) {\n    return createFromRawJson(\"Untitled\", urlOrJson);\n  }\n\n  // Wrapper for createFromRawJson to handle XML\n  // TODO ? change from urlOrJson to urlOrJsonOrXml\n  if (isXML(urlOrJson)) {\n    return createFromRawXml(\"Untitled\", urlOrJson);\n  }\n}\n\nexport async function createFromUrl(\n  url: URL,\n  title?: string,\n  options?: CreateJsonOptions\n): Promise<JSONDocument> {\n  if (options?.injest) {\n    const response = await safeFetch(url.href);\n\n    if (!response.ok) {\n      throw new Error(`Failed to injest ${url.href}`);\n    }\n\n    return createFromRawJson(title || url.href, await response.text(), options);\n  }\n\n  const docId = createId();\n\n  const doc: JSONDocument = {\n    id: docId,\n    type: <const>\"url\",\n    url: url.href,\n    title: title ?? url.hostname,\n    readOnly: options?.readOnly ?? false,\n  };\n\n  await DOCUMENTS.put(docId, JSON.stringify(doc), {\n    expirationTtl: options?.ttl ?? undefined,\n    metadata: options?.metadata ?? undefined,\n  });\n\n  return doc;\n}\n\nexport async function createFromRawJson(\n  filename: string,\n  contents: string,\n  options?: CreateJsonOptions\n): Promise<JSONDocument> {\n  const docId = createId();\n  const doc: JSONDocument = {\n    id: docId,\n    type: <const>\"raw\",\n    contents,\n    title: filename,\n    readOnly: options?.readOnly ?? false,\n  };\n\n  JSON.parse(contents);\n  await DOCUMENTS.put(docId, JSON.stringify(doc), {\n    expirationTtl: options?.ttl ?? undefined,\n    metadata: options?.metadata ?? undefined,\n  });\n\n  return doc;\n}\n\nexport async function getDocument(\n  slug: string\n): Promise<JSONDocument | undefined> {\n  const doc = await DOCUMENTS.get(slug);\n\n  if (!doc) return;\n\n  return JSON.parse(doc);\n}\n\nexport async function updateDocument(\n  slug: string,\n  title: string\n): Promise<JSONDocument | undefined> {\n  const document = await getDocument(slug);\n\n  if (!document) return;\n\n  const updated = { ...document, title };\n\n  await DOCUMENTS.put(slug, JSON.stringify(updated));\n\n  return updated;\n}\n\nexport async function deleteDocument(slug: string): Promise<void> {\n  await DOCUMENTS.delete(slug);\n}\n\nfunction createId(): string {\n  const nanoid = customRandom(\n    \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\",\n    12,\n    (bytes: number): Uint8Array => {\n      const array = new Uint8Array(bytes);\n      crypto.getRandomValues(array);\n      return array;\n    }\n  );\n  return nanoid();\n}\n\nfunction isUrl(possibleUrl: string): boolean {\n  try {\n    new URL(possibleUrl);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nfunction isJSON(possibleJson: string): boolean {\n  try {\n    JSON.parse(possibleJson);\n    return true;\n  } catch (e: any) {\n    throw new Error(e.message);\n  }\n}\n"
  },
  {
    "path": "app/root.tsx",
    "content": "import {\n  Links,\n  LiveReload,\n  LoaderFunction,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n  useLoaderData,\n  useLocation,\n} from \"remix\";\nimport type { MetaFunction } from \"remix\";\nimport clsx from \"clsx\";\nimport {\n  NonFlashOfWrongThemeEls,\n  Theme,\n  ThemeProvider,\n  useTheme,\n} from \"~/components/ThemeProvider\";\n\nimport openGraphImage from \"~/assets/images/opengraph.png\";\n\nexport const meta: MetaFunction = ({ location }) => {\n  const description =\n    \"JSON Hero makes reading and understand JSON files easy by giving you a clean and beautiful UI packed with extra features.\";\n  return {\n    title: \"JSON Hero - a beautiful JSON viewer for the web\",\n    viewport: \"width=device-width,initial-scale=1\",\n    description,\n    \"og:image\": `https://jsonhero.io${openGraphImage}`,\n    \"og:url\": `https://jsonhero.io${location.pathname}`,\n    \"og:title\": \"JSON Hero - A beautiful JSON viewer\",\n    \"og:description\": description,\n    \"twitter:image\": `https://jsonhero.io${openGraphImage}`,\n    \"twitter:card\": \"summary_large_image\",\n    \"twitter:creator\": \"@json_hero\",\n    \"twitter:site\": \"@json_hero\",\n    \"twitter:title\": \"JSON Hero\",\n    \"twitter:description\": description,\n  };\n};\n\nimport styles from \"./tailwind.css\";\nimport { getThemeSession } from \"./theme.server\";\nimport { getStarCount } from \"./services/github.server\";\nimport { StarCountProvider } from \"./components/StarCountProvider\";\nimport { PreferencesProvider } from \"~/components/PreferencesProvider\";\n\nexport function links() {\n  return [{ rel: \"stylesheet\", href: styles }];\n}\n\nexport type LoaderData = {\n  theme?: Theme;\n  starCount?: number;\n  themeOverride?: Theme;\n};\n\nexport const loader: LoaderFunction = async ({ request }) => {\n  const themeSession = await getThemeSession(request);\n  const starCount = await getStarCount();\n  const themeOverride = getThemeFromRequest(request);\n\n  const data: LoaderData = {\n    theme: themeSession.getTheme(),\n    starCount,\n    themeOverride,\n  };\n\n  return data;\n};\n\nfunction getThemeFromRequest(request: Request): Theme | undefined {\n  const url = new URL(request.url);\n  const theme = url.searchParams.get(\"theme\");\n  if (theme) {\n    return theme as Theme;\n  }\n  return undefined;\n}\n\nfunction App() {\n  const [theme] = useTheme();\n\n  return (\n    <html lang=\"en\" className={clsx(theme)}>\n      <head>\n        <Meta />\n        <meta charSet=\"utf-8\" />\n        <Links />\n        <NonFlashOfWrongThemeEls ssrTheme={Boolean(theme)} />\n      </head>\n      <body className=\"overscroll-none\">\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n        {process.env.NODE_ENV === \"development\" && <LiveReload />}\n      </body>\n    </html>\n  );\n}\n\nexport default function AppWithProviders() {\n  const { theme, starCount, themeOverride } = useLoaderData<LoaderData>();\n\n  const location = useLocation();\n\n  // Force dark mode on the homepage\n  const forceDarkMode = location.pathname === \"/\";\n\n  return (\n    <ThemeProvider\n      specifiedTheme={theme}\n      themeOverride={forceDarkMode ? \"dark\" : themeOverride}\n    >\n      <PreferencesProvider>\n        <StarCountProvider starCount={starCount}>\n          <App />\n        </StarCountProvider>\n      </PreferencesProvider>\n    </ThemeProvider>\n  );\n}\n"
  },
  {
    "path": "app/routes/actions/$id/update.ts",
    "content": "import { ActionFunction, json } from \"remix\";\nimport invariant from \"tiny-invariant\";\nimport { sendEvent } from \"~/graphJSON.server\";\nimport { updateDocument } from \"~/jsonDoc.server\";\n\nexport const action: ActionFunction = async ({ params, request, context }) => {\n  invariant(params.id, \"expected params.id\");\n\n  const title = (await request.formData()).get(\"title\");\n\n  invariant(typeof title === \"string\", \"expected title\");\n\n  try {\n    const document = await updateDocument(params.id, title);\n\n    if (!document) return json({ error: \"No document with that slug\" });\n\n    context.waitUntil(\n      sendEvent({\n        type: \"update-doc\",\n        id: document.id,\n        title,\n      })\n    );\n\n    return json(document);\n  } catch (error) {\n    if (error instanceof Error) {\n      return json({ error: error.message });\n    } else {\n      return json({ error: \"Unknown error\" });\n    }\n  }\n};\n"
  },
  {
    "path": "app/routes/actions/createFromFile.ts",
    "content": "import { ActionFunction, redirect } from \"remix\";\nimport invariant from \"tiny-invariant\";\nimport { sendEvent } from \"~/graphJSON.server\";\nimport { createFromRawJson } from \"~/jsonDoc.server\";\n\ntype CreateFromFileError = {\n  filename?: boolean;\n  rawJson?: boolean;\n};\n\nexport const action: ActionFunction = async ({ request, context }) => {\n  const formData = await request.formData();\n  const filename = formData.get(\"filename\");\n  const rawJson = formData.get(\"rawJson\");\n\n  const errors: CreateFromFileError = {};\n\n  if (!filename) errors.filename = true;\n  if (!rawJson) errors.rawJson = true;\n\n  if (Object.keys(errors).length) {\n    return errors;\n  }\n\n  invariant(typeof filename === \"string\", \"filename must be a string\");\n  invariant(typeof rawJson === \"string\", \"rawJson must be a string\");\n\n  const doc = await createFromRawJson(filename, rawJson);\n\n  const url = new URL(request.url);\n\n  context.waitUntil(\n    sendEvent({\n      type: \"create\",\n      from: \"file\",\n      id: doc.id,\n      source: url.searchParams.get(\"utm_source\") ?? url.hostname,\n    })\n  );\n\n  return redirect(`/j/${doc.id}`);\n};\n"
  },
  {
    "path": "app/routes/actions/createFromUrl.ts",
    "content": "import { redirect } from \"remix\";\nimport type { ActionFunction, LoaderFunction } from \"remix\";\nimport invariant from \"tiny-invariant\";\nimport { createFromUrl, createFromUrlOrRawJson } from \"~/jsonDoc.server\";\nimport { sendEvent } from \"~/graphJSON.server\";\nimport {\n  commitSession,\n  getSession,\n  setErrorMessage,\n} from \"../../services/toast.server\";\n\ntype CreateFromUrlError = {\n  jsonUrl?: boolean;\n};\n\nexport let action: ActionFunction = async ({ request, context }) => {\n  const formData = await request.formData();\n  const toastCookie = await getSession(request.headers.get(\"cookie\"));\n  const jsonUrl = formData.get(\"jsonUrl\");\n  const title = formData.get(\"title\") as string;\n\n  const errors: CreateFromUrlError = {};\n  if (!jsonUrl) errors.jsonUrl = true;\n\n  if (Object.keys(errors).length) {\n    return errors;\n  }\n\n  invariant(typeof jsonUrl === \"string\", \"jsonUrl must be a string\");\n\n  try {\n    const doc = await createFromUrlOrRawJson(jsonUrl, title);\n\n    if (!doc) {\n      setErrorMessage(\n        toastCookie,\n        \"Unknown error\",\n        \"Could not create document. Please try again.\"\n      );\n\n      return redirect(\"/\", {\n        headers: { \"Set-Cookie\": await commitSession(toastCookie) },\n      });\n    }\n\n    const requestUrl = new URL(request.url);\n\n    context.waitUntil(\n      sendEvent({\n        type: \"create\",\n        from: \"urlOrJson\",\n        id: doc.id,\n        source:\n          requestUrl.searchParams.get(\"utm_source\") ?? requestUrl.hostname,\n      })\n    );\n\n    return redirect(`/j/${doc.id}`);\n  } catch (e) {\n    if (e instanceof Error) {\n      setErrorMessage(toastCookie, e.message, \"Something went wrong\");\n    } else {\n      setErrorMessage(toastCookie, \"Unknown error\", \"Something went wrong\");\n    }\n\n    return redirect(\"/\", {\n      headers: { \"Set-Cookie\": await commitSession(toastCookie) },\n    });\n  }\n};\n\nexport let loader: LoaderFunction = async ({ request, context }) => {\n  const url = new URL(request.url);\n  const jsonUrl = url.searchParams.get(\"jsonUrl\");\n\n  if (!jsonUrl) {\n    return redirect(\"/\");\n  }\n\n  const jsonURL = new URL(jsonUrl);\n\n  invariant(jsonURL, \"jsonUrl must be a valid URL\");\n\n  const doc = await createFromUrl(jsonURL, jsonURL.href);\n\n  context.waitUntil(\n    sendEvent({\n      type: \"create\",\n      from: \"url\",\n      hostname: jsonURL.hostname,\n      id: doc.id,\n      source: url.searchParams.get(\"utm_source\") ?? url.hostname,\n    })\n  );\n\n  return redirect(`/j/${doc.id}`);\n};\n"
  },
  {
    "path": "app/routes/actions/getPreview.$url.ts",
    "content": "import { LoaderFunction } from \"remix\";\nimport invariant from \"tiny-invariant\";\nimport { getUriPreview } from \"~/services/uriPreview.server\";\n\nexport const loader: LoaderFunction = async ({ params }) => {\n  try {\n    invariant(params.url, \"expected params.url\");\n\n    const decoded = decodeURIComponent(params.url);\n\n    const earlyReturn = earlyRespondIfHomepagePreviewUri(decoded);\n\n    if (earlyReturn) {\n      return new Response(JSON.stringify(earlyReturn), {\n        headers: {\n          \"Content-Type\": \"application/json; charset=utf-8\",\n          \"Cache-Control\": \"public, max-age=3600\",\n        },\n      });\n    }\n\n    const result = await getUriPreview(decoded);\n\n    return new Response(JSON.stringify(result), {\n      headers: {\n        \"Content-Type\": \"application/json; charset=utf-8\",\n        \"Cache-Control\": \"public, max-age=3600\",\n      },\n    });\n  } catch {\n    return new Response(\n      JSON.stringify({ error: \"Unable to preview this URL\" }),\n      {\n        headers: {\n          \"Content-Type\": \"application/json; charset=utf-8\",\n          \"Cache-Control\": \"public, max-age=3600\",\n        },\n      }\n    );\n  }\n};\n\nfunction earlyRespondIfHomepagePreviewUri(uri: string) {\n  if (uri === \"https://www.theonion.com/\") {\n    return {\n      url: \"https://www.theonion.com/\",\n      domain: \"theonion.com\",\n      lastUpdated: \"2022-08-09T08:04:27.002858Z\",\n      nextUpdate: \"2022-08-10T08:04:24.888459Z\",\n      contentType: \"html\",\n      mimeType: \"text/html\",\n      size: 67994,\n      redirected: false,\n      title: \"The Onion | America's Finest News Source.\",\n      description:\n        \"The Onion brings you all of the latest news, stories, photos, videos and more from America's finest news source. \",\n      name: \"THEONION.COM\",\n      trackersDetected: false,\n      icon: {\n        url: \"https://cdn.peekalink.io/public/images/d9062cab-500b-4677-bd51-b08dae409d3b/b2dd179e-c3b3-4635-ba66-654835ada7b8.jpg\",\n        width: 200,\n        height: 200,\n      },\n      image: {\n        url: \"https://cdn.peekalink.io/public/images/d9062cab-500b-4677-bd51-b08dae409d3b/b2dd179e-c3b3-4635-ba66-654835ada7b8.jpg\",\n        width: 200,\n        height: 200,\n      },\n    };\n  }\n\n  if (uri === \"https://www.youtube.com/watch?v=dQw4w9WgXcQ\") {\n    return {\n      url: \"https://www.youtube.com/watch?v=dQw4w9WgXcQ\",\n      domain: \"youtube.com\",\n      lastUpdated: \"2022-08-09T08:04:28.028029Z\",\n      nextUpdate: \"2022-08-10T08:04:27.764732Z\",\n      contentType: \"html\",\n      mimeType: \"text/html\",\n      size: 499047,\n      redirected: true,\n      redirectionUrl: \"https://www.youtube.com/watch?ucbcb=1&v=dQw4w9WgXcQ\",\n      redirectionCount: 2,\n      redirectionTrail: [\n        \"https://consent.youtube.com/m?continue=https://www.youtube.com/watch?v=dQw4w9WgXcQ&gl=NL&hl=nl&m=0&pc=yt&src=1&uxe=23983172\",\n        \"https://www.youtube.com/watch?ucbcb=1&v=dQw4w9WgXcQ\",\n      ],\n      title: \"Rick Astley - Never Gonna Give You Up (Video)\",\n      description:\n        \"Rick Astley's official music video for “Never Gonna Give You Up” \\nListen to..\",\n      name: \"RickAstleyVEVO\",\n      trackersDetected: true,\n      icon: {\n        url: \"https://cdn.peekalink.io/public/images/66282716-f48d-40a9-933c-1d174f5a3180/a4696ad6-4a09-4ae0-b03d-2abb41323422.jpg\",\n        width: 48,\n        height: 48,\n      },\n      image: {\n        url: \"https://cdn.peekalink.io/public/images/0e1781f8-75dd-4930-91f5-e5c6a93facfe/efd883f6-3194-45ca-893a-cdec077c7de9.jpe\",\n        width: 480,\n        height: 360,\n      },\n      details: {\n        type: \"youtube\",\n        videoId: \"dQw4w9WgXcQ\",\n        duration: \"213.0\",\n        viewCount: 915578000,\n        likeCount: 9182302,\n        dislikeCount: 272157,\n        commentCount: 1519506,\n        publishedAt: \"2009-10-25T06:57:33Z\",\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "app/routes/actions/setTheme.ts",
    "content": "import { json, redirect } from \"remix\";\nimport type { ActionFunction, LoaderFunction } from \"remix\";\nimport { getThemeSession } from \"~/theme.server\";\nimport { isTheme } from \"~/components/ThemeProvider\";\nimport { sendEvent } from \"~/graphJSON.server\";\n\nexport const action: ActionFunction = async ({ request, context }) => {\n  const themeSession = await getThemeSession(request);\n  const requestText = await request.text();\n  const form = new URLSearchParams(requestText);\n  const theme = form.get(\"theme\");\n\n  if (!isTheme(theme)) {\n    return json({\n      success: false,\n      message: `theme value of ${theme} is not a valid theme`,\n    });\n  }\n\n  themeSession.setTheme(theme);\n\n  context.waitUntil(\n    sendEvent({\n      type: \"set-theme\",\n      theme,\n    })\n  );\n\n  return json(\n    { success: true },\n    { headers: { \"Set-Cookie\": await themeSession.commit() } }\n  );\n};\n\nexport const loader: LoaderFunction = () => redirect(\"/\", { status: 404 });\n"
  },
  {
    "path": "app/routes/api/create[.json].ts",
    "content": "import { ActionFunction, json, LoaderFunction } from \"remix\";\nimport invariant from \"tiny-invariant\";\nimport { sendEvent } from \"~/graphJSON.server\";\nimport { createFromRawJson, CreateJsonOptions } from \"~/jsonDoc.server\";\n\nexport const loader: LoaderFunction = async ({ request }) => {\n  if (request.method === \"OPTIONS\") {\n    return new Response(null, {\n      status: 204,\n      headers: {\n        \"Access-Control-Allow-Origin\": \"*\",\n        \"Access-Control-Allow-Methods\": \"POST\",\n        \"Access-Control-Allow-Headers\": \"Content-Type\",\n        \"Access-Control-Max-Age\": \"86400\",\n      },\n    });\n  }\n};\n\nexport const action: ActionFunction = async ({ request, context }) => {\n  const url = new URL(request.url);\n\n  const { title, content, ttl, readOnly } = await request.json();\n\n  if (!title || !content) {\n    return json({ message: \"Missing title or content\" }, 400);\n  }\n\n  invariant(typeof title === \"string\", \"title must be a string\");\n  invariant(content !== null, \"content cannot be null\");\n\n  const source = url.searchParams.get(\"utm_source\");\n\n  const options: CreateJsonOptions = {};\n\n  if (typeof ttl === \"number\") {\n    if (ttl < 60) {\n      return json({ message: \"ttl must be at least 60 seconds\" }, 400);\n    }\n\n    options.ttl = ttl;\n  }\n\n  if (typeof readOnly === \"boolean\") {\n    options.readOnly = readOnly;\n  }\n\n  const doc = await createFromRawJson(title, JSON.stringify(content), options);\n  url.pathname = `/j/${doc.id}`;\n\n  url.searchParams.delete(\"utm_source\");\n\n  context.waitUntil(\n    sendEvent({\n      type: \"create\",\n      from: \"url\",\n      hostname: url.hostname,\n      id: doc.id,\n      source,\n    })\n  );\n\n  return json(\n    { id: doc.id, title, location: url.toString() },\n    {\n      headers: {\n        \"Access-Control-Allow-Origin\": \"*\",\n      },\n    }\n  );\n};\n"
  },
  {
    "path": "app/routes/index.tsx",
    "content": "import { HomeCollaborateSection } from \"~/components/Home/HomeCollaborateSection\";\nimport { HomeEdgeCasesSection } from \"~/components/Home/HomeEdgeCasesSection\";\nimport { HomeFeatureGridSection } from \"~/components/Home/HomeFeatureGridSection\";\nimport { HomeHeader } from \"~/components/Home/HomeHeader\";\nimport { HomeHeroSection } from \"~/components/Home/HomeHeroSection\";\nimport { HomeInfoBoxSection } from \"~/components/Home/HomeInfoBoxSection\";\nimport { HomeSearchSection } from \"~/components/Home/HomeSearchSection\";\nimport { HomeFooter } from \"~/components/Home/HomeFooter\";\nimport {\n  commitSession,\n  getSession,\n  ToastMessage,\n} from \"../services/toast.server\";\nimport { json, useLoaderData } from \"remix\";\nimport ToastPopover from \"../components/UI/ToastPopover\";\nimport { HomeTriggerDevBanner } from \"~/components/Home/HomeTriggerDevBanner\";\n\ntype LoaderData = { toastMessage?: ToastMessage };\n\nexport async function loader({ request }: { request: Request }) {\n  const cookie = request.headers.get(\"cookie\");\n  const session = await getSession(cookie);\n  const toastMessage = session.get(\"toastMessage\") as ToastMessage;\n\n  return json(\n    { toastMessage },\n    {\n      headers: { \"Set-Cookie\": await commitSession(session) },\n    }\n  );\n}\nexport default function Index() {\n  const { toastMessage } = useLoaderData<LoaderData>();\n\n  return (\n    <div className=\"overflow-x-hidden\">\n      {toastMessage && (\n        <ToastPopover\n          message={toastMessage.message}\n          title={toastMessage.title}\n          type={toastMessage.type}\n          key={toastMessage.id}\n        />\n      )}\n\n      <HomeHeader fixed={true} />\n      <HomeHeroSection />\n      <HomeInfoBoxSection />\n      <HomeEdgeCasesSection />\n      <HomeSearchSection />\n      <HomeCollaborateSection />\n      <HomeFeatureGridSection />\n      <HomeFooter />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/routes/j/$id/editor.tsx",
    "content": "import { JsonEditor } from \"~/components/JsonEditor\";\n\nexport default function EditorView() {\n  return <JsonEditor />;\n}\n"
  },
  {
    "path": "app/routes/j/$id/index.tsx",
    "content": "import { JsonColumnView } from \"~/components/JsonColumnView\";\n\nexport default function DefaultView() {\n  return <JsonColumnView />;\n}\n"
  },
  {
    "path": "app/routes/j/$id/terminal.tsx",
    "content": "import { LargeTitle } from \"~/components/Primitives/LargeTitle\";\nimport { Body } from \"~/components/Primitives/Body\";\nimport { TerminalIcon } from \"@heroicons/react/outline\";\n\nexport default function TerminalViewPage() {\n  return (\n    <div className=\"flex items-center justify-center h-full\">\n      <div className=\"flex flex-col items-center justify-center max-w-[300px] rounded text-center bg-slate-200 shadow border-slate-100 border-[10px] border-solid py-16 px-16 transition dark:bg-slate-700 dark:border-slate-500\">\n        <TerminalIcon className=\"text-indigo-500 transition dark:text-white w-8 mb-2\" />\n        <LargeTitle className=\"text-gray-700 transition dark:text-white\">\n          Terminal View\n        </LargeTitle>\n        <Body className=\"text-gray-700 transition dark:text-white\">\n          Coming soon\n        </Body>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/routes/j/$id/tree.tsx",
    "content": "import { JsonTreeView } from \"~/components/JsonTreeView\";\n\nexport default function TreeViewPage() {\n  return <JsonTreeView />;\n}\n"
  },
  {
    "path": "app/routes/j/$id.tsx",
    "content": "import {\n  ActionFunction,\n  LoaderFunction,\n  MetaFunction,\n  Outlet,\n  redirect, ThrownResponse, useCatch,\n  useLoaderData,\n  useLocation,\n  useParams,\n} from \"remix\";\nimport invariant from \"tiny-invariant\";\nimport { deleteDocument, getDocument, JSONDocument } from \"~/jsonDoc.server\";\nimport { JsonDocProvider } from \"~/hooks/useJsonDoc\";\nimport { useEffect } from \"react\";\nimport { JsonProvider } from \"~/hooks/useJson\";\nimport { Footer } from \"~/components/Footer\";\nimport { Header } from \"~/components/Header\";\nimport { InfoPanel } from \"~/components/InfoPanel\";\nimport Resizable from \"~/components/Resizable\";\nimport { SideBar } from \"~/components/SideBar\";\nimport { JsonColumnViewProvider } from \"~/hooks/useJsonColumnView\";\nimport { JsonSchemaProvider } from \"~/hooks/useJsonSchema\";\nimport { JsonView } from \"~/components/JsonView\";\nimport safeFetch from \"~/utilities/safeFetch\";\nimport { JsonTreeViewProvider } from \"~/hooks/useJsonTree\";\nimport { JsonSearchProvider } from \"~/hooks/useJsonSearch\";\nimport { LargeTitle } from \"~/components/Primitives/LargeTitle\";\nimport { ExtraLargeTitle } from \"~/components/Primitives/ExtraLargeTitle\";\nimport { Body } from \"~/components/Primitives/Body\";\nimport { PageNotFoundTitle } from \"~/components/Primitives/PageNotFoundTitle\";\nimport { SmallSubtitle } from \"~/components/Primitives/SmallSubtitle\";\nimport { Logo } from \"~/components/Icons/Logo\";\nimport {\n  commitSession,\n  getSession,\n  setErrorMessage,\n  setSuccessMessage,\n} from \"~/services/toast.server\";\nimport { getRandomUserAgent } from '~/utilities/getRandomUserAgent'\n\nexport const loader: LoaderFunction = async ({ params, request }) => {\n  invariant(params.id, \"expected params.id\");\n\n  const doc = await getDocument(params.id);\n\n  if (!doc) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  const path = getPathFromRequest(request);\n  const minimal = getMinimalFromRequest(request);\n\n  if (doc.type == \"url\") {\n    console.log(`Fetching ${doc.url}...`);\n\n    const jsonResponse = await safeFetch(doc.url, {\n      headers: {\n        \"User-Agent\": getRandomUserAgent(),\n      },\n    });\n\n    if (!jsonResponse.ok) {\n      const jsonResponseText = await jsonResponse.text();\n      const error = `Failed to fetch ${doc.url}. HTTP status: ${jsonResponse.status} (${jsonResponseText}})`;\n      console.error(error);\n\n      throw new Response(error, {\n        status: jsonResponse.status,\n      });\n    }\n\n    const json = await jsonResponse.json();\n\n    return {\n      doc,\n      json,\n      path,\n      minimal,\n    };\n  } else {\n    return {\n      doc,\n      json: JSON.parse(doc.contents),\n      path,\n      minimal,\n    };\n  }\n};\n\nexport const action: ActionFunction = async ({ request, params }) => {\n  // Return if the request is not a DELETE\n  if (request.method !== \"DELETE\") {\n    return;\n  }\n\n  invariant(params.id, \"expected params.id\");\n\n  const toastCookie = await getSession(request.headers.get(\"cookie\"));\n\n  const document = await getDocument(params.id);\n\n  if (!document) {\n    setErrorMessage(toastCookie, \"Document not found\", \"Error\");\n\n    return redirect(`/`);\n  }\n\n  if (document.readOnly) {\n    setErrorMessage(toastCookie, \"Document is read-only\", \"Error\");\n\n    return redirect(`/j/${params.id}`);\n  }\n\n  await deleteDocument(params.id);\n\n  setSuccessMessage(toastCookie, \"Document deleted successfully\", \"Success\");\n\n  return redirect(\"/\", {\n    headers: { \"Set-Cookie\": await commitSession(toastCookie) },\n  });\n};\n\nfunction getPathFromRequest(request: Request): string | null {\n  const url = new URL(request.url);\n\n  const path = url.searchParams.get(\"path\");\n\n  if (!path) {\n    return null;\n  }\n\n  if (path.startsWith(\"$.\")) {\n    return path;\n  }\n\n  return `$.${path}`;\n}\n\nfunction getMinimalFromRequest(request: Request): boolean | undefined {\n  const url = new URL(request.url);\n\n  const minimal = url.searchParams.get(\"minimal\");\n\n  if (!minimal) {\n    return;\n  }\n\n  return minimal === \"true\";\n}\n\ntype LoaderData = {\n  doc: JSONDocument;\n  json: unknown;\n  path?: string;\n  minimal?: boolean;\n};\n\nexport const meta: MetaFunction = ({\n  data,\n}: {\n  data: LoaderData | undefined;\n}) => {\n  let title = \"JSON Hero\";\n\n  if (data?.doc?.title) {\n    title += ` - ${data.doc.title}`;\n  }\n\n  return {\n    title,\n    \"og:title\": title,\n    robots: \"noindex,nofollow\",\n  };\n};\n\nexport default function JsonDocumentRoute() {\n  const loaderData = useLoaderData<LoaderData>();\n\n  // Redirect back to `/j/${slug}` if the path is set, that way refreshing the page doesn't go to the path in the url.\n  const location = useLocation();\n\n  useEffect(() => {\n    if (loaderData.path) {\n      window.history.replaceState({}, \"\", location.pathname);\n    }\n  }, [loaderData.path]);\n\n  return (\n    <JsonDocProvider\n      doc={loaderData.doc}\n      path={loaderData.path}\n      key={loaderData.doc.id}\n      minimal={loaderData.minimal}\n    >\n      <JsonProvider initialJson={loaderData.json}>\n        <JsonSchemaProvider>\n          <JsonColumnViewProvider>\n            <JsonSearchProvider>\n              <JsonTreeViewProvider overscan={25}>\n                <div>\n                  <div className=\"block md:hidden fixed bg-black/80 h-screen w-screen z-50 text-white\">\n                    <div className=\"flex flex-col items-center justify-center h-full text-center\">\n                      <LargeTitle>JSON Hero only works on desktop</LargeTitle>\n                      <LargeTitle>👇</LargeTitle>\n                      <Body>(For now!)</Body>\n                      <a\n                        href=\"/\"\n                        className=\"mt-8 text-white bg-lime-500 rounded-sm px-4 py-2\"\n                      >\n                        Back to Home\n                      </a>\n                    </div>\n                  </div>\n                  <div className=\"h-screen flex flex-col sm:overflow-hidden\">\n                    {!loaderData.minimal && <Header />}\n                    <div className=\"bg-slate-50 flex-grow transition dark:bg-slate-900 overflow-y-auto\">\n                      <div className=\"main-container flex justify-items-stretch h-full\">\n                        <SideBar />\n                        <JsonView>\n                          <Outlet />\n                        </JsonView>\n\n                        <Resizable\n                          isHorizontal={true}\n                          initialSize={500}\n                          minimumSize={280}\n                          maximumSize={900}\n                        >\n                          <div className=\"info-panel flex-grow h-full\">\n                            <InfoPanel />\n                          </div>\n                        </Resizable>\n                      </div>\n                    </div>\n\n                    <Footer></Footer>\n                  </div>\n                </div>\n              </JsonTreeViewProvider>\n            </JsonSearchProvider>\n          </JsonColumnViewProvider>\n        </JsonSchemaProvider>\n      </JsonProvider>\n    </JsonDocProvider>\n  );\n}\n\nexport function CatchBoundary() {\n  const error = useCatch();\n  const params = useParams();\n  console.log(\"error\", error)\n\n  return (\n    <div className=\"flex items-center justify-center w-screen h-screen bg-[rgb(56,52,139)]\">\n      <div className=\"w-2/3\">\n        <div className=\"text-center text-lime-300\">\n          <div className=\"\">\n            <Logo />\n          </div>\n          <PageNotFoundTitle className=\"text-center leading-tight\">\n            {error.status}\n          </PageNotFoundTitle>\n        </div>\n        <div className=\"text-center leading-snug text-white\">\n          <ExtraLargeTitle className=\"text-slate-200 mb-8\">\n            <b>Sorry</b>! Something went wrong...\n          </ExtraLargeTitle>\n          <SmallSubtitle className=\"text-slate-200 mb-8\">\n            {error.data || (\n              error.status === 404\n                ? <>We couldn't find the page <b>'https://jsonhero.io/j/{params.id}'</b></>\n                : \"Unknown error occurred.\"\n            )}\n          </SmallSubtitle>\n          <a\n            href=\"/\"\n            className=\"mx-auto w-24 bg-lime-500 text-slate-900 text-lg font-bold px-5 py-1 rounded-sm uppercase whitespace-nowrap cursor-pointer opacity-90 hover:opacity-100 transition\"\n          >\n            HOME\n          </a>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/routes/j/$id[.json].ts",
    "content": "import { json, LoaderFunction } from \"remix\";\nimport invariant from \"tiny-invariant\";\nimport { getDocument } from \"~/jsonDoc.server\";\n\nexport const loader: LoaderFunction = async ({ params, request }) => {\n  invariant(params.id, \"expected params.id\");\n\n  const doc = await getDocument(params.id);\n\n  if (!doc) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  if (doc.type == \"url\") {\n    const jsonResponse = await fetch(doc.url);\n    return jsonResponse.json();\n  } else {\n    return json(JSON.parse(doc.contents));\n  }\n};\n"
  },
  {
    "path": "app/routes/new.tsx",
    "content": "import { json, LoaderFunction, redirect } from \"remix\";\nimport invariant from \"tiny-invariant\";\nimport { sendEvent } from \"~/graphJSON.server\";\nimport {\n  createFromRawJson,\n  createFromUrl,\n  CreateJsonOptions,\n} from \"~/jsonDoc.server\";\n\nexport let loader: LoaderFunction = async ({ request, context }) => {\n  const url = new URL(request.url);\n  const jsonUrl = url.searchParams.get(\"url\");\n  const base64EncodedJson = url.searchParams.get(\"j\");\n  const ttl = url.searchParams.get(\"ttl\");\n  const readOnly = url.searchParams.get(\"readonly\");\n  const title = url.searchParams.get(\"title\");\n  const injest = url.searchParams.get(\"injest\");\n\n  if (!jsonUrl && !base64EncodedJson) {\n    return redirect(\"/\");\n  }\n\n  const options: CreateJsonOptions = {};\n\n  if (typeof ttl === \"string\") {\n    invariant(ttl.match(/^\\d+$/), \"ttl must be a number\");\n\n    options.ttl = parseInt(ttl, 10);\n\n    invariant(options.ttl >= 60, \"ttl must be at least 60 seconds\");\n  }\n\n  if (typeof readOnly === \"string\") {\n    options.readOnly = readOnly === \"true\";\n  }\n\n  if (typeof injest === \"string\") {\n    options.injest = injest === \"true\";\n  }\n\n  if (jsonUrl) {\n    const jsonURL = new URL(jsonUrl);\n\n    invariant(jsonURL, \"url must be a valid URL\");\n\n    const doc = await createFromUrl(jsonURL, title ?? jsonURL.href, options);\n\n    context.waitUntil(\n      sendEvent({\n        type: \"create\",\n        from: \"url\",\n        hostname: jsonURL.hostname,\n        id: doc.id,\n        source: url.searchParams.get(\"utm_source\") ?? url.hostname,\n      })\n    );\n\n    return redirect(`/j/${doc.id}`);\n  }\n\n  if (base64EncodedJson) {\n    const doc = await createFromRawJson(\n      title ?? \"Untitled\",\n      atob(base64EncodedJson),\n      options\n    );\n\n    context.waitUntil(\n      sendEvent({\n        type: \"create\",\n        from: \"base64\",\n        id: doc.id,\n        source: url.searchParams.get(\"utm_source\"),\n      })\n    );\n\n    return redirect(`/j/${doc.id}`);\n  }\n};\n"
  },
  {
    "path": "app/routes/privacy.mdx",
    "content": "---\nmeta:\n  title: JSON Hero - Privacy\n  og:title: JSON Hero - Privacy\n---\n\nimport { HomeHeader } from \"~/components/Home/HomeHeader\";\n\n<HomeHeader />\n\n<div style={{width: \"800px\", marginLeft: \"12px\", marginTop: \"12px\", marginBottom: \"12px\"}}>\n**PRIVACY NOTICE**\n\n<br />\n\n### Last updated June 01, 2022\n\n<br />\n\nThis privacy notice for Stack Hero Limited (\"Company,\" \"we,\" \"us,\" or \"our\"), describes how and why we might collect, store, use, and/or share (\"process\") your information when you use our services (\"Services\"), such as when you:\nVisit our website at https://jsonhero.io, or any website of ours that links to this privacy notice\nEngage with us in other related ways, including any sales, marketing, or events\nQuestions or concerns? Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with our policies and practices, please do not use our Services. If you still have any questions or concerns, please contact us at hello@jsonhero.io.\n\n<br />\n\n**SUMMARY OF KEY POINTS**\n\n<br />\n\nThis summary provides key points from our privacy notice, but you can find out more details about any of these topics by clicking the link following each key point or by using our table of contents below to find the section you are looking for. You can also click here to go directly to our table of contents.\n\n<br />\n\nWhat personal information do we process? When you visit, use, or navigate our Services, we may process personal information depending on how you interact with Stack Hero Limited and the Services, the choices you make, and the products and features you use. Click here to learn more.\n\n<br />\n\nDo we process any sensitive personal information? We may process sensitive personal information when necessary with your consent or as otherwise permitted by applicable law. Click here to learn more.\n\n<br />\n\nDo we receive any information from third parties? We do not receive any information from third parties.\n\n<br />\n\nHow do we process your information? We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent. We process your information only when we have a valid legal reason to do so. Click here to learn more.\n\n<br />\n\nIn what situations and with which parties do we share personal information? We may share information in specific situations and with specific third parties. Click here to learn more.\n\n<br />\n\nHow do we keep your information safe? We have organizational and technical processes and procedures in place to protect your personal information. However, no electronic transmission over the internet or information storage technology can be guaranteed to be 100% secure, so we cannot promise or guarantee that hackers, cybercriminals, or other unauthorized third parties will not be able to defeat our security and improperly collect, access, steal, or modify your information. Click here to learn more.\n\n<br />\n\nWhat are your rights? Depending on where you are located geographically, the applicable privacy law may mean you have certain rights regarding your personal information. Click here to learn more.\n\n<br />\n\nHow do I exercise my rights? The easiest way to exercise your rights is by filling out our data subject request form available here: hello@jsonhero.io, or by contacting us. We will consider and act upon any request in accordance with applicable data protection laws.\n\n<br />\n\nWant to learn more about what Stack Hero Limited does with any information we collect? Click here to review the notice in full.\n\n<br />\n\n**1. WHAT INFORMATION DO WE COLLECT?**\n\n<br />\n\nIn Short: We collect personal information that you provide to us.\n\n<br />\n\nWe collect personal information that you voluntarily provide to us when you express an interest in obtaining information about us or our products and Services, when you participate in activities on the Services, or otherwise when you contact us.\n\n<br />\n\nSensitive Information. When necessary, with your consent or as otherwise permitted by applicable law, we process the following categories of sensitive information:\nAll personal information that you provide to us must be true, complete, and accurate, and you must notify us of any changes to such personal information.\n\n<br />\n\nInformation automatically collected\n\n<br />\n\nIn Short: Some information — such as your Internet Protocol (IP) address and/or browser and device characteristics — is collected automatically when you visit our Services.\n\n<br />\n\nWe automatically collect certain information when you visit, use, or navigate the Services. This information does not reveal your specific identity (like your name or contact information) but may include device and usage information, such as your IP address, browser and device characteristics, operating system, language preferences, referring URLs, device name, country, location, information about how and when you use our Services, and other technical information. This information is primarily needed to maintain the security and operation of our Services, and for our internal analytics and reporting purposes.\n\n<br />\n\nLike many businesses, we also collect information through cookies and similar technologies.\n\n<br />\n\nThe information we collect includes:\nLog and Usage Data. Log and usage data is service-related, diagnostic, usage, and performance information our servers automatically collect when you access or use our Services and which we record in log files. Depending on how you interact with us, this log data may include your IP address, device information, browser type, and settings and information about your activity in the Services (such as the date/time stamps associated with your usage, pages and files viewed, searches, and other actions you take such as which features you use), device event information (such as system activity, error reports (sometimes called \"crash dumps\"), and hardware settings).\n\n<br />\n\n**2. HOW DO WE PROCESS YOUR INFORMATION?**\n\n<br />\n\nIn Short: We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent.\n\n<br />\n\nWe process your personal information for a variety of reasons, depending on how you interact with our Services, including:\n\n<br />\n\nTo save or protect an individual's vital interest. We may process your information when necessary to save or protect an individual’s vital interest, such as to prevent harm.\n\n<br />\n\n**3. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR INFORMATION?**\n\n<br />\n\nIn Short: We only process your personal information when we believe it is necessary and we have a valid legal reason (i.e., legal basis) to do so under applicable law, like with your consent, to comply with laws, to provide you with services to enter into or fulfill our contractual obligations, to protect your rights, or to fulfill our legitimate business interests.\n\n<br />\n\nIf you are located in the EU or UK, this section applies to you.\n\nThe General Data Protection Regulation (GDPR) and UK GDPR require us to explain the valid legal bases we rely on in order to process your personal information. As such, we may rely on the following legal bases to process your personal information:\nConsent. We may process your information if you have given us permission (i.e., consent) to use your personal information for a specific purpose. You can withdraw your consent at any time. Click here to learn more.\nLegal Obligations. We may process your information where we believe it is necessary for compliance with our legal obligations, such as to cooperate with a law enforcement body or regulatory agency, exercise or defend our legal rights, or disclose your information as evidence in litigation in which we are involved.\nVital Interests. We may process your information where we believe it is necessary to protect your vital interests or the vital interests of a third party, such as situations involving potential threats to the safety of any person.\n\n<br />\n\nIf you are located in Canada, this section applies to you.\n\nWe may process your information if you have given us specific permission (i.e., express consent) to use your personal information for a specific purpose, or in situations where your permission can be inferred (i.e., implied consent). You can withdraw your consent at any time. Click here to learn more.\n\n<br />\n\nIn some exceptional cases, we may be legally permitted under applicable law to process your information without your consent, including, for example:\nIf collection is clearly in the interests of an individual and consent cannot be obtained in a timely way\nFor investigations and fraud detection and prevention\nFor business transactions provided certain conditions are met\nIf it is contained in a witness statement and the collection is necessary to assess, process, or settle an insurance claim\nFor identifying injured, ill, or deceased persons and communicating with next of kin\nIf we have reasonable grounds to believe an individual has been, is, or may be victim of financial abuse\nIf it is reasonable to expect collection and use with consent would compromise the availability or the accuracy of the information and the collection is reasonable for purposes related to investigating a breach of an agreement or a contravention of the laws of Canada or a province\nIf disclosure is required to comply with a subpoena, warrant, court order, or rules of the court relating to the production of records\nIf it was produced by an individual in the course of their employment, business, or profession and the collection is consistent with the purposes for which the information was produced\nIf the collection is solely for journalistic, artistic, or literary purposes\nIf the information is publicly available and is specified by the regulations\n\n<br />\n\n**4. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?**\n\n<br />\n\nIn Short: We may share information in specific situations described in this section and/or with the following third parties.\n\n<br />\n\nWe may need to share your personal information in the following situations:\nBusiness Transfers. We may share or transfer your information in connection with, or during negotiations of, any merger, sale of company assets, financing, or acquisition of all or a portion of our business to another company.\n\n<br />\n\n**5. DO WE USE COOKIES AND OTHER TRACKING TECHNOLOGIES?**\n\nIn Short: We may use cookies and other tracking technologies to collect and store your information.\n\n<br />\n\nWe may use cookies and similar tracking technologies (like web beacons and pixels) to access or store information. Specific information about how we use such technologies and how you can refuse certain cookies is set out in our Cookie Notice.\n\n<br />\n\n**6. HOW LONG DO WE KEEP YOUR INFORMATION?**\n\n<br />\n\nIn Short: We keep your information for as long as necessary to fulfill the purposes outlined in this privacy notice unless otherwise required by law.\n\n<br />\n\nWe will only keep your personal information for as long as it is necessary for the purposes set out in this privacy notice, unless a longer retention period is required or permitted by law (such as tax, accounting, or other legal requirements). No purpose in this notice will require us keeping your personal information for longer than 2 years.\n\n<br />\n\nWhen we have no ongoing legitimate business need to process your personal information, we will either delete or anonymize such information, or, if this is not possible (for example, because your personal information has been stored in backup archives), then we will securely store your personal information and isolate it from any further processing until deletion is possible.\n\n<br />\n\n**7. HOW DO WE KEEP YOUR INFORMATION SAFE?**\n\n<br />\n\nIn Short: We aim to protect your personal information through a system of organizational and technical security measures.\n\n<br />\n\nWe have implemented appropriate and reasonable technical and organizational security measures designed to protect the security of any personal information we process. However, despite our safeguards and efforts to secure your information, no electronic transmission over the Internet or information storage technology can be guaranteed to be 100% secure, so we cannot promise or guarantee that hackers, cybercriminals, or other unauthorized third parties will not be able to defeat our security and improperly collect, access, steal, or modify your information. Although we will do our best to protect your personal information, transmission of personal information to and from our Services is at your own risk. You should only access the Services within a secure environment.\n\n<br />\n\n**8. WHAT ARE YOUR PRIVACY RIGHTS?**\n\n<br />\n\nIn Short: In some regions, such as the European Economic Area (EEA), United Kingdom (UK), and Canada, you have rights that allow you greater access to and control over your personal information. You may review, change, or terminate your account at any time.\n\n<br />\n\nIn some regions (like the EEA, UK, and Canada), you have certain rights under applicable data protection laws. These may include the right (i) to request access and obtain a copy of your personal information, (ii) to request rectification or erasure; (iii) to restrict the processing of your personal information; and (iv) if applicable, to data portability. In certain circumstances, you may also have the right to object to the processing of your personal information. You can make such a request by contacting us by using the contact details provided in the section \"HOW CAN YOU CONTACT US ABOUT THIS NOTICE?\" below.\n\n<br />\n\nWe will consider and act upon any request in accordance with applicable data protection laws.\n\n<br />\n\nIf you are located in the EEA or UK and you believe we are unlawfully processing your personal information, you also have the right to complain to your local data protection supervisory authority. You can find their contact details here: https://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm.\n\n<br />\n\nIf you are located in Switzerland, the contact details for the data protection authorities are available here: https://www.edoeb.admin.ch/edoeb/en/home.html.\n\n<br />\n\nWithdrawing your consent: If we are relying on your consent to process your personal information, which may be express and/or implied consent depending on the applicable law, you have the right to withdraw your consent at any time. You can withdraw your consent at any time by contacting us by using the contact details provided in the section \"HOW CAN YOU CONTACT US ABOUT THIS NOTICE?\" below.\n\n<br />\n\nHowever, please note that this will not affect the lawfulness of the processing before its withdrawal, nor when applicable law allows, will it affect the processing of your personal information conducted in reliance on lawful processing grounds other than consent.\n\n<br />\n\nCookies and similar technologies: Most Web browsers are set to accept cookies by default. If you prefer, you can usually choose to set your browser to remove cookies and to reject cookies. If you choose to remove cookies or reject cookies, this could affect certain features or services of our Services. To opt out of interest-based advertising by advertisers on our Services visit http://www.aboutads.info/choices/.\n\n<br />\n\nIf you have questions or comments about your privacy rights, you may email us at hello@jsonhero.io.\n\n<br />\n\n**9. CONTROLS FOR DO-NOT-TRACK FEATURES**\n\n<br />\n\nMost web browsers and some mobile operating systems and mobile applications include a Do-Not-Track (\"DNT\") feature or setting you can activate to signal your privacy preference not to have data about your online browsing activities monitored and collected. At this stage no uniform technology standard for recognizing and implementing DNT signals has been finalized. As such, we do not currently respond to DNT browser signals or any other mechanism that automatically communicates your choice not to be tracked online. If a standard for online tracking is adopted that we must follow in the future, we will inform you about that practice in a revised version of this privacy notice.\n\n<br />\n\n**10. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS?**\n\n<br />\n\nIn Short: Yes, if you are a resident of California, you are granted specific rights regarding access to your personal information.\n\n<br />\n\nCalifornia Civil Code Section 1798.83, also known as the \"Shine The Light\" law, permits our users who are California residents to request and obtain from us, once a year and free of charge, information about categories of personal information (if any) we disclosed to third parties for direct marketing purposes and the names and addresses of all third parties with which we shared personal information in the immediately preceding calendar year. If you are a California resident and would like to make such a request, please submit your request in writing to us using the contact information provided below.\n\n<br />\n\nIf you are under 18 years of age, reside in California, and have a registered account with Services, you have the right to request removal of unwanted data that you publicly post on the Services. To request removal of such data, please contact us using the contact information provided below and include the email address associated with your account and a statement that you reside in California. We will make sure the data is not publicly displayed on the Services, but please be aware that the data may not be completely or comprehensively removed from all our systems (e.g., backups, etc.).\n\n<br />\n\n**11. DO WE MAKE UPDATES TO THIS NOTICE?**\n\n<br />\n\nIn Short: Yes, we will update this notice as necessary to stay compliant with relevant laws.\n\n<br />\n\nWe may update this privacy notice from time to time. The updated version will be indicated by an updated \"Revised\" date and the updated version will be effective as soon as it is accessible. If we make material changes to this privacy notice, we may notify you either by prominently posting a notice of such changes or by directly sending you a notification. We encourage you to review this privacy notice frequently to be informed of how we are protecting your information.\n\n<br />\n\n**12. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?**\n\n<br />\n\nIf you have questions or comments about this notice, you may email us at hello@jsonhero.io or by post to:\n\n<br />\n\nStack Hero Limited\n68 Hanbury St\nLondon, England E1 5JL\nUnited Kingdom\n\n<br />\n\n**13. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU?**\n\n<br />\n\nBased on the applicable laws of your country, you may have the right to request access to the personal information we collect from you, change that information, or delete it in some circumstances. To request to review, update, or delete your personal information, please visit: hello@jsonhero.io.\n\n</div>\n"
  },
  {
    "path": "app/services/apihero.server.ts",
    "content": "import { createFetchProxy } from \"@apihero/fetch\";\n\nexport const fetchProxy =\n  typeof APIHERO_PROJECT_KEY === \"string\"\n    ? createFetchProxy({\n        projectKey: APIHERO_PROJECT_KEY,\n        env: process.env.NODE_ENV,\n      })\n    : fetch;\n"
  },
  {
    "path": "app/services/github.server.ts",
    "content": "export async function getStarCount(): Promise<number | undefined> {\n  try {\n    const response = await fetch(\n      `https://api.github.com/repos/triggerdotdev/jsonhero-web`,\n      {\n        headers: {\n          accept: \"application/json\",\n          \"user-agent\":\n            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36\",\n        },\n        cf: {\n          cacheEverything: true,\n          cacheTtlByStatus: {\n            \"200-299\": 300,\n            \"400-499\": 5,\n            \"500-599\": 0,\n          },\n        },\n      }\n    );\n\n    if (!response.ok) {\n      console.error(`Could not fetch star count: ${response.statusText}`);\n\n      return;\n    }\n\n    const data = await response.json();\n    return data.stargazers_count;\n  } catch (error) {\n    console.error(error);\n    return;\n  }\n}\n"
  },
  {
    "path": "app/services/toast.server.ts",
    "content": "import { createCookieSessionStorage, Session } from \"remix\";\n\nexport type ToastMessage = {\n  message: string;\n  title: string;\n  type: \"success\" | \"error\";\n  id: string;\n};\n\nconst ONE_YEAR = 1000 * 60 * 60 * 24 * 365;\n\nexport const { commitSession, getSession } = createCookieSessionStorage({\n  cookie: {\n    name: \"__message\",\n    path: \"/\",\n    httpOnly: true,\n    sameSite: \"lax\",\n    maxAge: ONE_YEAR,\n    secrets: [SESSION_SECRET],\n    secure: true,\n  },\n});\n\nexport function setSuccessMessage(\n  session: Session,\n  message: string,\n  title: string\n) {\n  session.flash(\"toastMessage\", {\n    message,\n    title,\n    type: \"success\",\n    id: crypto.randomUUID(),\n  });\n}\n\nexport function setErrorMessage(\n  session: Session,\n  message: string,\n  title: string\n) {\n  session.flash(\"toastMessage\", {\n    message,\n    title,\n    type: \"error\",\n    id: crypto.randomUUID(),\n  });\n}\n"
  },
  {
    "path": "app/services/uriPreview.server.ts",
    "content": "import {\n  PreviewImage,\n  PreviewJson,\n  PreviewResult,\n  OpenGraphPreviewData,\n  OpenGraphPreviewDataError\n} from \"~/components/Preview/Types/preview.types\";\nimport safeFetch from \"~/utilities/safeFetch\";\nimport { fetchProxy } from \"./apihero.server\";\n\nconst imageContentTypes = [\n  \"image/jpeg\",\n  \"image/png\",\n  \"image/gif\",\n  \"image/webp\",\n  \"image/svg+xml\",\n];\n\nasync function getOpenGraphNinja(link: string): Promise<PreviewResult> {\n  const response = await fetchProxy(`https://opengraph.ninja/api/v1?url=${link}`);\n\n  if (response.ok) {\n    const body: OpenGraphPreviewData = await response.json();\n    return {\n      url: body.requestUrl,\n      contentType: 'html',\n      mimeType: 'text/html',\n      title: body.title,\n      description: body.description,\n      icon: { url: body.details.favicon ?? '' },\n      image: {\n        url: body.image?.url ?? '',\n        alt: body.image?.alt\n      }\n    };\n  } else {\n    const body: OpenGraphPreviewDataError = await response.json();\n\n    // Log the error instead of propagating the internal error to the UI\n    console.log(`OpenGraph Ninja failed to get preview data: ${body.error}`);\n\n    return { error: \"No preview available for this URL\" };\n  }\n}\n\nexport async function getUriPreview(uri: string): Promise<PreviewResult> {\n  const url = rewriteUrl(uri);\n\n  const head = await headUri(url.href);\n\n  // If the url is an image content type, return a preview image\n  if (\n    head &&\n    imageContentTypes.some((contentType) =>\n      contentType.includes(head.contentType)\n    )\n  ) {\n    const previewImage = createPreviewImage(url.href, head);\n\n    return previewImage;\n  }\n\n  // If the url is a json content type, attempt to request the json and return a preview json\n  if (head?.contentType.includes(\"application/json\")) {\n    const response = await safeFetch(url.href, {\n      headers: {\n        accept: \"application/json\",\n      },\n    });\n\n    if (!response.ok) {\n      return { error: \"No preview available for this URL\" };\n    }\n\n    const jsonBody = await response.json();\n\n    return createPreviewJson(url.href, jsonBody);\n  }\n\n  return await getOpenGraphNinja(url.href);\n}\n\ntype HeadInfo = {\n  contentType: string;\n  contentLength: number;\n  lastModified: string;\n};\n\nasync function headUri(\n  uri: string,\n  redirectCount = 0\n): Promise<HeadInfo | undefined> {\n  const response = await fetch(uri, {\n    method: \"HEAD\",\n    headers: {\n      accept: \"*/*\",\n      \"user-agent\":\n        \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36\",\n    },\n  });\n\n  if (!response.ok) {\n    // If this is a 405 Method Not Allowed, do a GET request instead and if that is a redirect, return the head of the redirect url\n    if (response.status === 405 && redirectCount < 5) {\n      // Do a GET request that does not follow redirects\n      const noFollowResponse = await fetch(uri, {\n        method: \"GET\",\n        redirect: \"manual\",\n        headers: {\n          accept: \"*/*\",\n          \"user-agent\":\n            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36\",\n        },\n      });\n\n      if (noFollowResponse.status === 301 || noFollowResponse.status === 302) {\n        // Get the url from the response Location header\n        const location = noFollowResponse.headers.get(\"location\");\n\n        if (location) {\n          return headUri(location, redirectCount + 1);\n        }\n      }\n    }\n\n    return;\n  }\n\n  return {\n    contentType: response.headers.get(\"content-type\") || \"\",\n    contentLength: Number(response.headers.get(\"content-length\") || \"0\"),\n    lastModified: response.headers.get(\"last-modified\") || \"\",\n  };\n}\n\nfunction createPreviewJson(uri: string, json: unknown): PreviewJson {\n  return {\n    url: uri,\n    contentType: \"json\",\n    json,\n  };\n}\n\nfunction createPreviewImage(uri: string, head: HeadInfo): PreviewImage {\n  return {\n    url: uri,\n    contentType: \"image\",\n    mimeType: head.contentType,\n    size: head.contentLength,\n  };\n}\n\n// Rewrites the URL to convert an ipfs: url to use https://ipfs.io/ipfs/\n// Rewrites the URL to convert an git: url to use https://\nfunction rewriteUrl(url: string): URL {\n  const unmodifiedUrl = new URL(url);\n\n  // Rewrite the URL if it is a relative URL\n  if (unmodifiedUrl.protocol === \"ipfs:\") {\n    if (unmodifiedUrl.hostname === \"\") {\n      return new URL(\n        `https://ipfs.io/ipfs/${unmodifiedUrl.pathname.substring(2)}`\n      );\n    } else {\n      // Parse out the \"hostname\" from the raw url because hostnames are case-insensitive and automatically lowercased\n      const urlMatches = url.match(/^ipfs:\\/\\/([A-Za-z0-9]+)(\\/.*)?/i);\n      const hostname = urlMatches?.[1];\n\n      return new URL(\n        `https://ipfs.io/ipfs/${hostname ?? unmodifiedUrl.hostname}${\n          unmodifiedUrl.pathname.length > 0 ? `/${unmodifiedUrl.pathname}` : \"\"\n        }${unmodifiedUrl.search}`\n      );\n    }\n  }\n  if (unmodifiedUrl.protocol === \"git:\") {\n    return new URL(\n      `https://${unmodifiedUrl.hostname}${unmodifiedUrl.pathname.replace(\n        \".git\",\n        \"\"\n      )}`\n    );\n  }\n\n  return unmodifiedUrl;\n}\n"
  },
  {
    "path": "app/theme.server.ts",
    "content": "import { createCookieSessionStorage } from \"remix\";\n\nimport { Theme, isTheme } from \"~/components/ThemeProvider\";\n\nconst sessionSecret = SESSION_SECRET;\n\nconst themeStorage = createCookieSessionStorage({\n  cookie: {\n    name: \"theme-cookie\",\n    secure: true,\n    secrets: [sessionSecret],\n    sameSite: \"lax\",\n    path: \"/\",\n    httpOnly: true,\n  },\n});\n\nasync function getThemeSession(request: Request) {\n  const session = await themeStorage.getSession(request.headers.get(\"Cookie\"));\n  return {\n    getTheme: () => {\n      const themeValue = session.get(\"theme\");\n      return isTheme(themeValue) ? themeValue : \"dark\";\n    },\n    setTheme: (theme: Theme) => session.set(\"theme\", theme),\n    commit: () => themeStorage.commitSession(session),\n  };\n}\n\nexport { getThemeSession };\n"
  },
  {
    "path": "app/useColumnView/index.ts",
    "content": "import { omit } from \"lodash-es\";\nimport React, {\n  ReactNode,\n  Reducer,\n  useCallback,\n  useEffect,\n  useMemo,\n  useReducer,\n} from \"react\";\nimport { useMemoCompare } from \"~/hooks/useMemoCompare\";\n\nexport type IconComponent = (\n  props: React.SVGProps<SVGSVGElement>\n) => JSX.Element;\n\nexport interface ColumnViewNode {\n  id: string;\n  name: string;\n  title: string;\n  subtitle?: string;\n  longTitle?: string;\n  icon?: IconComponent;\n  children: ColumnViewNode[];\n}\n\nexport type ColumnViewOptions = {\n  rootNode: ColumnViewNode;\n  initialState?: ColumnViewState | string;\n  stateReducer?: ColumnViewStateReducerHook;\n};\n\nexport type ColumnDefinition = {\n  id: string;\n  title: string;\n  icon?: IconComponent;\n  items: ColumnViewNode[];\n};\n\nexport type ColumnViewInstanceState = {\n  columns: Array<ColumnDefinition>;\n  getColumnViewProps: () => ColumnViewProps;\n  selectedNodeId?: string;\n  selectedNodeSource?: string;\n  selectedPath: string[];\n  highlightedNodeId?: string;\n  highlightedPath: string[];\n  selectedNodes: ColumnViewNode[];\n  canGoBack: boolean;\n  canGoForward: boolean;\n};\n\nexport type ColumnViewAPIOptions = {\n  source?: KeyboardEvent | MouseEvent;\n};\n\nexport type ColumnViewAPI = {\n  goBack: () => void;\n  goForward: () => void;\n  goToNodeId: (nodeId: string, source: string) => void;\n  goToParent: (options?: ColumnViewAPIOptions) => void;\n  goToChildren: () => void;\n  goToNextSibling: () => void;\n  goToPreviousSibling: () => void;\n  resetSelection: () => void;\n};\n\nexport type ColumnViewInstance = {\n  state: ColumnViewInstanceState;\n  api: ColumnViewAPI;\n};\n\nexport type ColumnViewProps = {\n  children?: ReactNode;\n  className?: string;\n  tabIndex?: number;\n};\n\ntype ColumnDefinitionCache = Map<string, ColumnDefinition>;\n\nexport function useColumnView({\n  rootNode,\n  initialState,\n  stateReducer,\n}: ColumnViewOptions): ColumnViewInstance {\n  const columnCache = React.useRef<ColumnDefinitionCache>(new Map());\n\n  useEffect(() => {\n    columnCache.current = new Map();\n  }, [rootNode]);\n\n  const nodeTable = useMemo<NodeTable>(\n    () => generateNodeTable(rootNode),\n    [rootNode]\n  );\n\n  const enhancedReducer: Reducer<ColumnViewState, ColumnViewAction> =\n    useCallback(\n      (state: ColumnViewState, action: ColumnViewAction): ColumnViewState => {\n        let changes = columnViewReducer(state, action);\n\n        // Don't allow the client modify history actions\n        if (action.type === \"GO\") {\n          return changes;\n        }\n\n        if (stateReducer) {\n          changes = stateReducer(state, action, changes);\n        }\n\n        //we need to get rid of any history items further forward because the user has forked by navigating\n        let updatedHistory = changes.history.slice(\n          0,\n          changes.historyCurrentIndex + 1\n        );\n        let historyIndex = changes.historyCurrentIndex;\n\n        //add new entry\n        const newHistoryEntry = omit(changes, \"history\", \"historyCurrentIndex\");\n        updatedHistory.push(newHistoryEntry);\n        historyIndex = updatedHistory.length - 1;\n\n        return {\n          ...changes,\n          history: updatedHistory,\n          historyCurrentIndex: historyIndex,\n        };\n      },\n      [stateReducer]\n    );\n\n  const [state, dispatch] = useReducer<\n    Reducer<ColumnViewState, ColumnViewAction>\n  >(\n    enhancedReducer,\n    typeof initialState === \"string\"\n      ? {\n          selectedNodeId: initialState,\n          highlightedNodeId: initialState,\n          history: [],\n          historyCurrentIndex: 0,\n          nodeTable,\n          rootNodeId: rootNode.id,\n        }\n      : initialState ?? {\n          selectedNodeId: \"$\",\n          highlightedNodeId: \"$\",\n          history: [],\n          historyCurrentIndex: 0,\n          nodeTable,\n          rootNodeId: rootNode.id,\n        }\n  );\n\n  const api = useMemo<ColumnViewAPI>(() => {\n    return {\n      goBack: () => {\n        dispatch(goBackAction());\n      },\n      goForward: () => {\n        dispatch(goForwardAction());\n      },\n      goToNodeId: (nodeId: string, source: string) => {\n        dispatch(goToNodeIdAction(nodeId, source));\n      },\n      goToParent: (options?: ColumnViewAPIOptions) => {\n        dispatch(goToParentAction(options));\n      },\n      goToChildren: () => {\n        dispatch(goToChildrenAction());\n      },\n      goToPreviousSibling: () => {\n        dispatch(goToPreviousSibling());\n      },\n      goToNextSibling: () => {\n        dispatch(goToNextSibling());\n      },\n      resetSelection: () => {\n        dispatch(resetSelectionAction());\n      },\n    };\n  }, [dispatch]);\n\n  const {\n    selectedNodeId,\n    highlightedNodeId,\n    selectedNodeSource,\n    history,\n    historyCurrentIndex,\n  } = state;\n\n  const selectedPath = getPathToNode(nodeTable, selectedNodeId);\n  const highlightedPath = getPathToNode(nodeTable, highlightedNodeId);\n\n  const columns = useMemoCompare(\n    generateColumns(nodeTable, selectedPath, columnCache.current),\n    (previous, next) => {\n      if (!previous || !next) return false;\n      if (previous.length !== next.length) return false;\n      const isEqual =\n        previous.map(({ id }) => id).join(\"\") ===\n        next.map(({ id }) => id).join(\"\");\n\n      return isEqual;\n    }\n  );\n  const selectedNodes = selectedPath.map((id) => nodeTable[id].node);\n\n  const getColumnViewProps = useCallback(() => {\n    return {};\n  }, []);\n\n  const canGoBack = historyCurrentIndex > 0;\n  const canGoForward = historyCurrentIndex < history.length - 1;\n\n  return {\n    state: {\n      selectedNodeId,\n      selectedNodeSource,\n      selectedPath,\n      selectedNodes,\n      highlightedNodeId,\n      highlightedPath,\n      columns: columns ?? [],\n      getColumnViewProps,\n      canGoBack,\n      canGoForward,\n    },\n    api,\n  };\n}\n\nexport type ColumnViewState = {\n  selectedNodeId?: string;\n  highlightedNodeId?: string;\n  selectedNodeSource?: string;\n  history: Array<Omit<ColumnViewState, \"history\" | \"historyCurrentIndex\">>;\n  historyCurrentIndex: number;\n  nodeTable: NodeTable;\n  rootNodeId: string;\n};\n\nexport type SetSelectedNodeIdAction = {\n  type: \"SET_SELECTED_NODE_ID\";\n  id: string;\n  source: string;\n};\n\nexport type MoveSelectedNodeAction = {\n  type: \"MOVE_UP\" | \"MOVE_DOWN\" | \"MOVE_TO_PARENT\" | \"MOVE_TO_CHILDREN\";\n  source?: KeyboardEvent | MouseEvent;\n};\n\nexport type ResetSelectionNodeAction = {\n  type: \"RESET_SELECTION\";\n};\n\nexport type GoAction = {\n  type: \"GO\";\n  direction: -1 | 1;\n};\n\nexport type ColumnViewAction =\n  | SetSelectedNodeIdAction\n  | MoveSelectedNodeAction\n  | ResetSelectionNodeAction\n  | GoAction;\n\nfunction goBackAction(): GoAction {\n  return {\n    type: \"GO\",\n    direction: -1,\n  };\n}\n\nfunction goForwardAction(): GoAction {\n  return {\n    type: \"GO\",\n    direction: 1,\n  };\n}\n\nfunction resetSelectionAction(): ResetSelectionNodeAction {\n  return {\n    type: \"RESET_SELECTION\",\n  };\n}\n\nfunction goToNodeIdAction(\n  nodeId: string,\n  source: string\n): SetSelectedNodeIdAction {\n  return {\n    type: \"SET_SELECTED_NODE_ID\",\n    id: nodeId,\n    source,\n  };\n}\n\nfunction goToParentAction(\n  options?: ColumnViewAPIOptions\n): MoveSelectedNodeAction {\n  return {\n    type: \"MOVE_TO_PARENT\",\n    source: options?.source,\n  };\n}\n\nfunction goToChildrenAction(): MoveSelectedNodeAction {\n  return {\n    type: \"MOVE_TO_CHILDREN\",\n  };\n}\n\nfunction goToPreviousSibling(): MoveSelectedNodeAction {\n  return {\n    type: \"MOVE_UP\",\n  };\n}\n\nfunction goToNextSibling(): MoveSelectedNodeAction {\n  return {\n    type: \"MOVE_DOWN\",\n  };\n}\n\nexport type ColumnViewStateReducerHook = (\n  state: ColumnViewState,\n  action: ColumnViewAction,\n  changes: ColumnViewState\n) => ColumnViewState;\n\n/*\n  Needs to support the following selection actions:\n    1. Select a node by id\n    2. Highlight a node by id\n    3. Move to a specific child of of the current node\n    4. Move to the next sibling of the current node\n    5. Move to the previous sibling of the current node\n    5. Move to the parent of the current node\n    6. Move to the root of the current node\n    7. Move to the first child of the current node\n    8. Move back to the previous state\n    9. Move forward to the next state\n*/\nfunction columnViewReducer(\n  state: ColumnViewState,\n  action: ColumnViewAction\n): ColumnViewState {\n  switch (action.type) {\n    case \"SET_SELECTED_NODE_ID\":\n      return {\n        ...state,\n        selectedNodeId: action.id,\n        highlightedNodeId: action.id,\n        selectedNodeSource: action.source,\n      };\n    case \"MOVE_DOWN\": {\n      if (state.highlightedNodeId === state.rootNodeId) {\n        return moveToChildren(state);\n      }\n\n      const id = getHighlightedSibling(state, state.nodeTable, 1);\n\n      if (!id) {\n        return state;\n      }\n\n      return {\n        ...state,\n        selectedNodeId: id,\n        highlightedNodeId: id,\n      };\n    }\n    case \"MOVE_UP\": {\n      const id = getHighlightedSibling(state, state.nodeTable, -1);\n\n      if (!id) {\n        return state;\n      }\n\n      return {\n        ...state,\n        selectedNodeId: id,\n        highlightedNodeId: id,\n      };\n    }\n    case \"MOVE_TO_CHILDREN\": {\n      return moveToChildren(state);\n    }\n    case \"MOVE_TO_PARENT\": {\n      const { highlightedNodeId } = state;\n\n      if (!highlightedNodeId) {\n        return state;\n      }\n\n      const highlightedNode = state.nodeTable[highlightedNodeId];\n\n      if (!highlightedNode || !highlightedNode.parentId) {\n        return state;\n      }\n\n      const id = highlightedNode.parentId;\n\n      return {\n        ...state,\n        selectedNodeId: id,\n        highlightedNodeId: id,\n      };\n    }\n    case \"RESET_SELECTION\": {\n      if (state.selectedNodeId !== state.highlightedNodeId) {\n        return {\n          ...state,\n          selectedNodeId: state.highlightedNodeId,\n        };\n      }\n\n      break;\n    }\n    case \"GO\": {\n      const { history, historyCurrentIndex } = state;\n\n      if (action.direction === -1 && historyCurrentIndex > 0) {\n        const newHistoryCurrentIndex = historyCurrentIndex - 1;\n        const nextState = history[newHistoryCurrentIndex];\n\n        return {\n          ...nextState,\n          historyCurrentIndex: newHistoryCurrentIndex,\n          history,\n        };\n      }\n\n      if (action.direction === 1 && historyCurrentIndex < history.length - 1) {\n        const newHistoryCurrentIndex = historyCurrentIndex + 1;\n        const nextState = history[newHistoryCurrentIndex];\n\n        return {\n          ...nextState,\n          historyCurrentIndex: newHistoryCurrentIndex,\n          history,\n        };\n      }\n\n      break;\n    }\n    default:\n      return state;\n  }\n\n  return state;\n}\n\nfunction moveToChildren(state: ColumnViewState): ColumnViewState {\n  const { highlightedNodeId } = state;\n\n  if (!highlightedNodeId) {\n    return state;\n  }\n\n  const highlightedNode = state.nodeTable[highlightedNodeId];\n\n  if (!highlightedNode || highlightedNode.children.length === 0) {\n    return state;\n  }\n\n  const id = highlightedNode.children[0];\n\n  return {\n    ...state,\n    selectedNodeId: id,\n    highlightedNodeId: id,\n  };\n}\n\n// TODO: CACHE THIS\nfunction generateColumns(\n  nodeTable: NodeTable,\n  path: string[],\n  columnCache: ColumnDefinitionCache\n): Array<ColumnDefinition> {\n  const columns: Array<ColumnDefinition> = [];\n\n  function addColumn(nodeRecord: NodeRecord) {\n    const cachedColumn = columnCache.get(nodeRecord.id);\n\n    if (cachedColumn) {\n      columns.push(cachedColumn);\n      return;\n    }\n\n    const column: ColumnDefinition = {\n      id: nodeRecord.id,\n      title: nodeRecord.node.longTitle ?? nodeRecord.node.title,\n      icon: nodeRecord.node.icon,\n      items: nodeRecord.node.children || [],\n    };\n\n    columns.push(column);\n\n    columnCache.set(nodeRecord.id, column);\n  }\n\n  path.forEach((nodeId) => {\n    const nodeRecord = nodeTable[nodeId];\n\n    if (nodeRecord && nodeRecord.node.children.length > 0) {\n      addColumn(nodeRecord);\n    }\n  });\n\n  return columns;\n}\n\nfunction getPathToNode(nodeTable: NodeTable, nodeId?: string): string[] {\n  return getNodeAncestorPath(nodeTable, nodeId).reverse();\n}\n\nfunction getNodeAncestorPath(\n  nodeTable: NodeTable,\n  nodeId?: string,\n  path: string[] = []\n): string[] {\n  if (!nodeId) {\n    return path;\n  }\n\n  const nodeRecord = nodeTable[nodeId];\n\n  if (!nodeRecord) {\n    return path;\n  }\n\n  return getNodeAncestorPath(\n    nodeTable,\n    nodeRecord.parentId,\n    path.concat(nodeId)\n  );\n}\n\ntype NodeRecord = {\n  id: string;\n  node: ColumnViewNode;\n  parentId?: string;\n  children: string[];\n};\n\ntype NodeTable = {\n  [id: string]: NodeRecord;\n};\n\nfunction generateNodeTable(rootNode: ColumnViewNode): NodeTable {\n  const nodesById: { [id: string]: NodeRecord } = {};\n\n  function addNode(node: ColumnViewNode, parentId?: string) {\n    const nodeRecord: NodeRecord = {\n      id: node.id,\n      node,\n      parentId,\n      children: [],\n    };\n\n    nodesById[node.id] = nodeRecord;\n\n    if (node.children) {\n      node.children.forEach((child) => {\n        addNode(child, node.id);\n      });\n\n      nodeRecord.children = node.children.map((child) => child.id);\n    }\n  }\n\n  addNode(rootNode);\n\n  return nodesById;\n}\n\nfunction getHighlightedSibling(\n  state: ColumnViewState,\n  nodeTable: NodeTable,\n  direction: -1 | 1\n): string | undefined {\n  const { highlightedNodeId } = state;\n\n  if (!highlightedNodeId) {\n    return;\n  }\n\n  const highlightedNode = nodeTable[highlightedNodeId];\n\n  if (!highlightedNode || !highlightedNode.parentId) {\n    return;\n  }\n\n  const parentNode = nodeTable[highlightedNode.parentId];\n\n  if (!parentNode) {\n    return;\n  }\n\n  const highlightedIndex = parentNode.children.indexOf(highlightedNodeId);\n  const nextIndex = highlightedIndex + direction;\n\n  if (parentNode.children.length <= nextIndex || nextIndex < 0) {\n    return;\n  }\n\n  const nextNodeId = parentNode.children[nextIndex];\n\n  return nextNodeId;\n}\n"
  },
  {
    "path": "app/utilities/animationConstants.ts",
    "content": "export const transition = {\n  type: \"spring\",\n  stiffness: 200,\n  damping: 10,\n};\n\nexport const whileTap = {\n  scale: 0.95,\n  rotate: 15,\n};\n"
  },
  {
    "path": "app/utilities/classnames.ts",
    "content": "export default function classnames(...args: string[]): string {\n  return args.filter(Boolean).join(\" \");\n}\n"
  },
  {
    "path": "app/utilities/codeMirrorSetup.ts",
    "content": "import {\n  highlightSpecialChars,\n  drawSelection,\n  highlightActiveLine,\n  dropCursor,\n} from \"@codemirror/view\";\nimport { Extension } from \"@codemirror/state\";\nimport { highlightActiveLineGutter } from \"@codemirror/gutter\";\nimport { bracketMatching } from \"@codemirror/matchbrackets\";\nimport { highlightSelectionMatches } from \"@codemirror/search\";\nimport { json as jsonLang } from \"@codemirror/lang-json\";\nimport { lineNumbers } from \"@codemirror/gutter\";\n\nexport function getPreviewSetup(): Array<Extension> {\n  return [\n    jsonLang(),\n    highlightSpecialChars(),\n    drawSelection(),\n    dropCursor(),\n    bracketMatching(),\n    highlightSelectionMatches(),\n    lineNumbers(),\n  ];\n}\n\nexport function getViewerSetup(): Array<Extension> {\n  return [drawSelection(), dropCursor(), bracketMatching(), lineNumbers()];\n}\n\nexport function getEditorSetup(): Array<Extension> {\n  return [\n    highlightActiveLineGutter(),\n    highlightSpecialChars(),\n    drawSelection(),\n    dropCursor(),\n    bracketMatching(),\n    highlightActiveLine(),\n    highlightSelectionMatches(),\n    lineNumbers(),\n  ];\n}\n"
  },
  {
    "path": "app/utilities/codeMirrorTheme.ts",
    "content": "import { EditorView } from \"@codemirror/view\";\nimport { Extension } from \"@codemirror/state\";\nimport { HighlightStyle, tags as t } from \"@codemirror/highlight\";\n\nexport function darkTheme(): Extension {\n  const chalky = \"#e5c07b\",\n    coral = \"#e06c75\",\n    cyan = \"#56b6c2\",\n    invalid = \"#ffffff\",\n    ivory = \"#abb2bf\",\n    stone = \"#7d8799\",\n    malibu = \"#61afef\",\n    sage = \"#98c379\",\n    whiskey = \"#d19a66\",\n    violet = \"#c678dd\",\n    darkBackground = \"#21252b\",\n    highlightBackground = \"rgba(234,179,8,0.3)\",\n    background = \"rgb(15,23,42)\",\n    tooltipBackground = \"#353a42\",\n    selection = \"#3E4451\",\n    cursor = \"#528bff\";\n\n  const jsonHeroEditorTheme = EditorView.theme(\n    {\n      \"&\": {\n        color: ivory,\n        backgroundColor: background,\n      },\n\n      \".cm-content\": {\n        caretColor: cursor,\n        fontFamily: \"MonoLisa, monospace\",\n        fontSize: \"14px\",\n      },\n\n      \".cm-cursor, .cm-dropCursor\": { borderLeftColor: cursor },\n      \"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection\":\n        { backgroundColor: selection },\n\n      \".cm-panels\": { backgroundColor: darkBackground, color: ivory },\n      \".cm-panels.cm-panels-top\": { borderBottom: \"2px solid black\" },\n      \".cm-panels.cm-panels-bottom\": { borderTop: \"2px solid black\" },\n\n      \".cm-searchMatch\": {\n        backgroundColor: \"#72a1ff59\",\n        outline: \"1px solid #457dff\",\n      },\n      \".cm-searchMatch.cm-searchMatch-selected\": {\n        backgroundColor: \"#6199ff2f\",\n      },\n\n      \".cm-activeLine\": { backgroundColor: highlightBackground },\n      \".cm-selectionMatch\": { backgroundColor: \"#aafe661a\" },\n\n      \"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket\": {\n        backgroundColor: \"#bad0f847\",\n        outline: \"1px solid #515a6b\",\n      },\n\n      \".cm-gutters\": {\n        backgroundColor: background,\n        color: stone,\n        border: \"none\",\n      },\n\n      \".cm-activeLineGutter\": {\n        backgroundColor: highlightBackground,\n      },\n\n      \".cm-foldPlaceholder\": {\n        backgroundColor: \"transparent\",\n        border: \"none\",\n        color: \"#ddd\",\n      },\n\n      \".cm-tooltip\": {\n        border: \"none\",\n        backgroundColor: tooltipBackground,\n      },\n      \".cm-tooltip .cm-tooltip-arrow:before\": {\n        borderTopColor: \"transparent\",\n        borderBottomColor: \"transparent\",\n      },\n      \".cm-tooltip .cm-tooltip-arrow:after\": {\n        borderTopColor: tooltipBackground,\n        borderBottomColor: tooltipBackground,\n      },\n      \".cm-tooltip-autocomplete\": {\n        \"& > ul > li[aria-selected]\": {\n          backgroundColor: highlightBackground,\n          color: ivory,\n        },\n      },\n    },\n    { dark: true }\n  );\n\n  /// The highlighting style for code in the JSON Hero theme.\n  const jsonHeroHighlightStyle = HighlightStyle.define([\n    { tag: t.keyword, color: violet },\n    {\n      tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],\n      color: coral,\n    },\n    { tag: [t.function(t.variableName), t.labelName], color: malibu },\n    { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },\n    { tag: [t.definition(t.name), t.separator], color: ivory },\n    {\n      tag: [\n        t.typeName,\n        t.className,\n        t.number,\n        t.changed,\n        t.annotation,\n        t.modifier,\n        t.self,\n        t.namespace,\n      ],\n      color: chalky,\n    },\n    {\n      tag: [\n        t.operator,\n        t.operatorKeyword,\n        t.url,\n        t.escape,\n        t.regexp,\n        t.link,\n        t.special(t.string),\n      ],\n      color: cyan,\n    },\n    { tag: [t.meta, t.comment], color: stone },\n    { tag: t.strong, fontWeight: \"bold\" },\n    { tag: t.emphasis, fontStyle: \"italic\" },\n    { tag: t.strikethrough, textDecoration: \"line-through\" },\n    { tag: t.link, color: stone, textDecoration: \"underline\" },\n    { tag: t.heading, fontWeight: \"bold\", color: coral },\n    { tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },\n    { tag: [t.processingInstruction, t.string, t.inserted], color: sage },\n    { tag: t.invalid, color: invalid },\n  ]);\n\n  return [jsonHeroEditorTheme, jsonHeroHighlightStyle];\n}\n\nexport function lightTheme(): Extension {\n  const chalky = \"#e5c07b\",\n    coral = \"#e06c75\",\n    cyan = \"#56b6c2\",\n    invalid = \"#ffffff\",\n    ivory = \"#abb2bf\",\n    stone = \"#7d8799\",\n    malibu = \"#61afef\",\n    sage = \"#98c379\",\n    whiskey = \"#d19a66\",\n    violet = \"#c678dd\",\n    darkBackground = \"#21252b\",\n    highlightBackground = \"#D0D0D0\",\n    background = \"rgb(248,250,252)\",\n    tooltipBackground = \"#353a42\",\n    selection = \"#D0D0D0\",\n    cursor = \"#528bff\";\n\n  const jsonHeroEditorTheme = EditorView.theme(\n    {\n      \"&\": {\n        color: ivory,\n        backgroundColor: background,\n      },\n\n      \".cm-content\": {\n        caretColor: cursor,\n        fontFamily: \"MonoLisa, monospace\",\n        fontSize: \"14px\",\n      },\n\n      \".cm-cursor, .cm-dropCursor\": { borderLeftColor: cursor },\n      \"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection\":\n        { backgroundColor: selection },\n\n      \".cm-panels\": { backgroundColor: darkBackground, color: ivory },\n      \".cm-panels.cm-panels-top\": { borderBottom: \"2px solid black\" },\n      \".cm-panels.cm-panels-bottom\": { borderTop: \"2px solid black\" },\n\n      \".cm-searchMatch\": {\n        backgroundColor: \"#72a1ff59\",\n        outline: \"1px solid #457dff\",\n      },\n      \".cm-searchMatch.cm-searchMatch-selected\": {\n        backgroundColor: \"#6199ff2f\",\n      },\n\n      \".cm-activeLine\": { backgroundColor: highlightBackground },\n      \".cm-selectionMatch\": { backgroundColor: \"#aafe661a\" },\n\n      \"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket\": {\n        backgroundColor: \"#bad0f847\",\n        outline: \"1px solid #515a6b\",\n      },\n\n      \".cm-gutters\": {\n        backgroundColor: background,\n        color: stone,\n        border: \"none\",\n      },\n\n      \".cm-activeLineGutter\": {\n        backgroundColor: highlightBackground,\n      },\n\n      \".cm-foldPlaceholder\": {\n        backgroundColor: \"transparent\",\n        border: \"none\",\n        color: \"#ddd\",\n      },\n\n      \".cm-tooltip\": {\n        border: \"none\",\n        backgroundColor: tooltipBackground,\n      },\n      \".cm-tooltip .cm-tooltip-arrow:before\": {\n        borderTopColor: \"transparent\",\n        borderBottomColor: \"transparent\",\n      },\n      \".cm-tooltip .cm-tooltip-arrow:after\": {\n        borderTopColor: tooltipBackground,\n        borderBottomColor: tooltipBackground,\n      },\n      \".cm-tooltip-autocomplete\": {\n        \"& > ul > li[aria-selected]\": {\n          backgroundColor: highlightBackground,\n          color: ivory,\n        },\n      },\n    },\n    { dark: false }\n  );\n\n  /// The highlighting style for code in the JSON Hero theme.\n  const jsonHeroHighlightStyle = HighlightStyle.define([\n    { tag: t.keyword, color: violet },\n    {\n      tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],\n      color: coral,\n    },\n    { tag: [t.function(t.variableName), t.labelName], color: malibu },\n    { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },\n    { tag: [t.definition(t.name), t.separator], color: ivory },\n    {\n      tag: [\n        t.typeName,\n        t.className,\n        t.number,\n        t.changed,\n        t.annotation,\n        t.modifier,\n        t.self,\n        t.namespace,\n      ],\n      color: chalky,\n    },\n    {\n      tag: [\n        t.operator,\n        t.operatorKeyword,\n        t.url,\n        t.escape,\n        t.regexp,\n        t.link,\n        t.special(t.string),\n      ],\n      color: cyan,\n    },\n    { tag: [t.meta, t.comment], color: stone },\n    { tag: t.strong, fontWeight: \"bold\" },\n    { tag: t.emphasis, fontStyle: \"italic\" },\n    { tag: t.strikethrough, textDecoration: \"line-through\" },\n    { tag: t.link, color: stone, textDecoration: \"underline\" },\n    { tag: t.heading, fontWeight: \"bold\", color: coral },\n    { tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },\n    { tag: [t.processingInstruction, t.string, t.inserted], color: sage },\n    { tag: t.invalid, color: invalid },\n  ]);\n\n  return [jsonHeroEditorTheme, jsonHeroHighlightStyle];\n}\n"
  },
  {
    "path": "app/utilities/colors.ts",
    "content": "import { inferType } from \"@jsonhero/json-infer-types\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\n\nexport function colorForTypeName(typeName: string): string {\n  switch (typeName) {\n    case \"object\": {\n      return \"text-emerald-500\";\n    }\n    case \"array\": {\n      return \"text-cyan-500\";\n    }\n    case \"null\": {\n      return \"text-stone-400\";\n    }\n    case \"bool\": {\n      return \"text-rose-500\";\n    }\n    case \"int\":\n    case \"float\": {\n      return \"text-amber-500\";\n    }\n    case \"string\": {\n      return \"text-indigo-500\";\n    }\n    default: {\n      return \"\";\n    }\n  }\n}\n\nexport function colorForItemAtPath(path: string, json: unknown): string {\n  const heroPath = new JSONHeroPath(path);\n  const value = heroPath.first(json);\n  const item = inferType(value);\n\n  return colorForTypeName(item.name);\n}\n"
  },
  {
    "path": "app/utilities/dataType.ts",
    "content": "import { JSONValueType } from \"@jsonhero/json-infer-types\";\n\nexport interface HierarchicalTypes {\n  types: string[];\n}\n\nexport function concatenated(types: HierarchicalTypes): string {\n  return types.types.join(\"/\");\n}\n\nexport function getHierarchicalTypes(type: JSONValueType): HierarchicalTypes {\n  let types: string[] = [];\n  types.push(type.name);\n\n  switch (type.name) {\n    case \"string\": {\n      if (type.format == null) {\n        break;\n      }\n\n      types.push(type.format.name);\n\n      switch (type.format.name) {\n        case \"uri\": {\n          if (type.format.contentType == null) {\n            break;\n          }\n\n          types.push(type.format.contentType);\n          break;\n        }\n        case \"datetime\": {\n          types.push(type.format.variant);\n          break;\n        }\n        case \"ip\": {\n          types.push(type.format.variant);\n          break;\n        }\n      }\n      break;\n    }\n    case \"int\": {\n      if (type.format == null) {\n        break;\n      }\n\n      types.push(type.format.name);\n    }\n  }\n  return {\n    types: types,\n  };\n}\n"
  },
  {
    "path": "app/utilities/formatStarCount.ts",
    "content": "// Should truncate the number to not show the exact number of stars\n// If over 1000, show 1k\n// Round up to the nearest hundred, so for example, 1150 becomes 1.2k, 1101 becomes 1.1k, etc.\nexport function formatStarCount(count: number | undefined): string {\n  if (count === undefined) {\n    return \"⭐️\";\n  }\n\n  if (count < 1000) {\n    return count.toString();\n  }\n\n  return `${roundWithPrecision(count / 1000, 1)}k`;\n}\n\nfunction roundWithPrecision(value: number, precision: number): number {\n  const multiplier = Math.pow(10, precision);\n  return Math.round(value * multiplier) / multiplier;\n}\n"
  },
  {
    "path": "app/utilities/formatter.ts",
    "content": "import { Temporal } from \"@js-temporal/polyfill\";\nimport { inferTemporal } from \"./inferredTemporal\";\n\nimport {\n  JSONDateTimeFormat,\n  JSONStringFormat,\n  JSONValueType,\n} from \"@jsonhero/json-infer-types\";\n\nexport function formatRawValue(type: JSONValueType): string {\n  switch (type.name) {\n    case \"string\":\n      return type.value;\n    case \"int\":\n      return type.value.toString();\n    case \"float\":\n      return type.value.toString();\n    case \"bool\":\n      return type.value ? \"true\" : \"false\";\n    case \"null\":\n      return \"null\";\n    case \"array\":\n      return \"[]\";\n    case \"object\":\n      return \"{}\";\n  }\n}\n\nexport type FormatValueOptions = {\n  leafNodesOnly?: boolean;\n};\n\nexport function formatValue(\n  type: JSONValueType,\n  options?: FormatValueOptions\n): string | undefined {\n  switch (type.name) {\n    case \"array\": {\n      if (options?.leafNodesOnly) {\n        return;\n      }\n\n      if (type.value.length == 0) {\n        return formatRawValue(type);\n      } else if (type.value.length === 1) {\n        return `1 item`;\n      } else {\n        return `${type.value.length} items`;\n      }\n    }\n    case \"object\": {\n      if (options?.leafNodesOnly) {\n        return;\n      }\n\n      if (Object.keys(type.value).length == 0) {\n        return formatRawValue(type);\n      } else if (Object.keys(type.value).length === 1) {\n        return `1 field`;\n      } else {\n        return `${Object.keys(type.value).length} fields`;\n      }\n    }\n    case \"bool\": {\n      return type.value ? \"true\" : \"false\";\n    }\n    case \"float\":\n    case \"int\":\n      return formatNumber(type.value);\n    case \"null\": {\n      return \"null\";\n    }\n    case \"string\":\n      return formatString(type.value, type.format);\n    default:\n      const _exhaustiveCheck: never = type;\n      return _exhaustiveCheck;\n  }\n}\n\nconst numberFormatter = new Intl.NumberFormat(undefined, {\n  maximumFractionDigits: 6,\n});\n\nexport function formatNumber(value: number): string {\n  return numberFormatter.format(value);\n}\n\nfunction formatString(value: string, format?: JSONStringFormat): string {\n  if (!format) {\n    return value;\n  }\n\n  switch (format.name) {\n    case \"email\":\n      return value;\n    case \"uri\":\n      return value;\n    case \"datetime\":\n      return formatDateTime(value, format);\n    default:\n      return value;\n  }\n}\n\nexport function formatDateTime(\n  value: string,\n  format?: JSONDateTimeFormat\n): string {\n  if (!format) {\n    return value;\n  }\n\n  const temporal = inferTemporal(value, format);\n\n  if (!temporal) {\n    return value;\n  }\n\n  switch (format.parts) {\n    case \"datetime\":\n      return temporal.toLocaleString(\"en-US\", {\n        year: \"numeric\",\n        month: \"short\",\n        day: \"numeric\",\n        hour: \"numeric\",\n        minute: \"numeric\",\n        second: \"numeric\",\n        timeZoneName: \"short\",\n      });\n    case \"date\":\n      return temporal.toLocaleString(\"en-US\", {\n        year: \"numeric\",\n        month: \"short\",\n        day: \"numeric\",\n      });\n    case \"time\":\n      return temporal.toLocaleString(\"en-US\", {\n        hour: \"numeric\",\n        minute: \"numeric\",\n        second: \"numeric\",\n      });\n  }\n}\n\nexport function formatBytes(bytes: number, decimals = 2): string {\n  if (bytes === 0) return \"0 Bytes\";\n\n  const k = 1024;\n  const dm = decimals < 0 ? 0 : decimals;\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"];\n\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + \" \" + sizes[i];\n}\n"
  },
  {
    "path": "app/utilities/getRandomUserAgent.ts",
    "content": "const userAgents = [\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 PageSpeedPlus/1.0.0\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 11.6; rv:92.0) Gecko/20100101 Firefox/92.0\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.14 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/25.0 Chrome/121.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:125.0) Gecko/20100101 Firefox/125.0\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.16 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.5.3 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15\",\n  \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.5.12 Chrome/120.0.6099.283 Electron/28.2.3 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.16 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 OPR/109.0.0.0\",\n  \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36\",\n  \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.13 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15\",\n  \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15\",\n  \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.13 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.1 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.5.12 Chrome/120.0.6099.283 Electron/28.2.3 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.2 Safari/605.1.15\",\n  \"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Mobile/15E148 Safari/604.1\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.5.3 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15\",\n  \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36\",\n  \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\n  \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/117.0.0.0 Mobile Safari/537.36\"\n];\n\nexport function getRandomUserAgent() {\n  return userAgents[Math.floor(Math.random() * userAgents.length)];\n}\n"
  },
  {
    "path": "app/utilities/icons.ts",
    "content": "import {\n  CubeIcon,\n  CollectionIcon,\n  EyeOffIcon,\n  CheckCircleIcon,\n  AnnotationIcon,\n  CalendarIcon,\n  AtSymbolIcon,\n  GlobeAltIcon,\n  PhotographIcon,\n  CodeIcon,\n  PhoneIcon,\n  DocumentIcon,\n  ColorSwatchIcon,\n  CreditCardIcon,\n  CurrencyDollarIcon,\n  ClockIcon,\n  GlobeIcon,\n  EmojiHappyIcon,\n  ChatAlt2Icon,\n  ArchiveIcon,\n  IdentificationIcon,\n  KeyIcon,\n  DocumentTextIcon,\n  HashtagIcon,\n} from \"@heroicons/react/outline\";\nimport { inferType, JSONValueType } from \"@jsonhero/json-infer-types\";\nimport { StringIcon } from \"~/components/Icons/StringIcon\";\nimport { IconComponent } from \"~/useColumnView\";\n\nexport function iconForValue(value: unknown): IconComponent {\n  return iconForType(inferType(value));\n}\n\nexport function iconForType(type: JSONValueType): IconComponent {\n  switch (type.name) {\n    case \"object\": {\n      return CubeIcon;\n    }\n    case \"array\": {\n      return CollectionIcon;\n    }\n    case \"null\": {\n      return EyeOffIcon;\n    }\n    case \"bool\": {\n      return CheckCircleIcon;\n    }\n    case \"int\": {\n      if (type.format == null) {\n        return HashtagIcon;\n      }\n      switch (type.format.name) {\n        case \"timestamp\": {\n          return CalendarIcon;\n        }\n        default: {\n          return HashtagIcon;\n        }\n      }\n    }\n    case \"float\": {\n      return HashtagIcon;\n    }\n    case \"string\": {\n      if (type.format == null) {\n        return StringIcon;\n      }\n\n      switch (type.format.name) {\n        case \"timestamp\": {\n          return CalendarIcon;\n        }\n        case \"datetime\": {\n          switch (type.format.parts) {\n            case \"time\":\n              return ClockIcon;\n          }\n          return ClockIcon;\n        }\n        case \"email\": {\n          return AtSymbolIcon;\n        }\n        case \"hostname\":\n        case \"tld\":\n        case \"ip\":\n          return GlobeAltIcon;\n        case \"uri\": {\n          switch (type.format.contentType) {\n            case \"image/jpeg\":\n            case \"image/png\":\n            case \"image/gif\":\n            case \"image/webm\":\n              return PhotographIcon;\n            case \"application/json\":\n              return CodeIcon;\n            default:\n              return GlobeAltIcon;\n          }\n        }\n        case \"phoneNumber\": {\n          return PhoneIcon;\n        }\n        case \"currency\": {\n          return CurrencyDollarIcon;\n        }\n        case \"country\": {\n          return GlobeIcon;\n        }\n        case \"emoji\": {\n          return EmojiHappyIcon;\n        }\n        case \"color\": {\n          return ColorSwatchIcon;\n        }\n        case \"language\": {\n          return ChatAlt2Icon;\n        }\n        case \"filesize\": {\n          return ArchiveIcon;\n        }\n        case \"uuid\": {\n          return IdentificationIcon;\n        }\n        case \"json\":\n        case \"jsonPointer\": {\n          return CodeIcon;\n        }\n        case \"jwt\": {\n          return KeyIcon;\n        }\n        case \"semver\": {\n          return DocumentTextIcon;\n        }\n        case \"creditcard\": {\n          switch (type.format.variant) {\n            case \"visa\": {\n              return CreditCardIcon;\n            }\n            case \"mastercard\": {\n              return CreditCardIcon;\n            }\n            case \"amex\": {\n              return CreditCardIcon;\n            }\n            case \"discover\": {\n              return CreditCardIcon;\n            }\n            case \"dinersclub\": {\n              return CreditCardIcon;\n            }\n            default: {\n              return CreditCardIcon;\n            }\n          }\n        }\n        default: {\n          return AnnotationIcon;\n        }\n      }\n    }\n    default: {\n      return DocumentIcon;\n    }\n  }\n}\n"
  },
  {
    "path": "app/utilities/inferredTemporal.ts",
    "content": "import { Temporal } from \"@js-temporal/polyfill\";\nimport { JSONDateTimeFormat } from \"@jsonhero/json-infer-types\";\n\nexport type InferredTemporal =\n  | Temporal.Instant\n  | Temporal.ZonedDateTime\n  | Temporal.PlainDateTime\n  | Temporal.PlainDate\n  | Temporal.PlainTime;\n\nexport function inferTemporal(\n  value: string,\n  format: JSONDateTimeFormat\n): InferredTemporal | undefined {\n  if (format.variant === \"rfc2822\") {\n    return;\n  }\n\n  try {\n    switch (format.parts) {\n      case \"datetime\": {\n        if (format.extensions && format.extensions.includes(\"timezone\")) {\n          return Temporal.ZonedDateTime.from(value);\n        }\n\n        try {\n          return Temporal.Instant.from(value);\n        } catch {\n          return Temporal.PlainDateTime.from(value);\n        }\n      }\n      case \"date\": {\n        try {\n          return Temporal.Instant.from(value);\n        } catch {\n          return Temporal.PlainDate.from(value);\n        }\n      }\n      case \"time\": {\n        try {\n          return Temporal.Instant.from(value);\n        } catch {\n          return Temporal.PlainTime.from(value);\n        }\n      }\n    }\n  } catch (e) {\n    console.error(e);\n\n    return;\n  }\n}\n"
  },
  {
    "path": "app/utilities/jsonColumnView.ts",
    "content": "import { inferType, JSONValueType } from \"@jsonhero/json-infer-types\";\nimport { JSONHeroPath, PathComponent } from \"@jsonhero/path\";\nimport { ColumnViewNode } from \"~/useColumnView\";\nimport { formatValue } from \"./formatter\";\nimport { iconForType } from \"./icons\";\n\nexport function generateColumnViewNode(json: unknown): ColumnViewNode {\n  const info = inferType(json);\n  const path = new JSONHeroPath(\"$\");\n  const children = generateChildren(info, path);\n\n  return {\n    name: \"root\",\n    title: \"root\",\n    id: \"$\",\n    icon: iconForType(info),\n    children,\n  };\n}\n\nfunction generateChildren(\n  info: JSONValueType,\n  path: JSONHeroPath\n): Array<ColumnViewNode> {\n  if (info.name === \"array\" && info.value) {\n    return info.value.map((value, index) => {\n      const childPath = path.child(index.toString());\n      const childInfo = inferType(value);\n      const children = generateChildren(childInfo, childPath);\n\n      return {\n        id: childPath.toString(),\n        name: index.toString(),\n        title: index.toString(),\n        longTitle: `Index ${index.toString()}`,\n        subtitle: formatValue(childInfo),\n        icon: iconForType(childInfo),\n        children,\n      };\n    });\n  }\n\n  if (info.name === \"object\" && info.value) {\n    return Object.entries(info.value).map(([key, value]) => {\n      const cleanKey = key.replace(/\\./g, \"\\\\.\");\n      const childPath = path.child(cleanKey);\n      const childInfo = inferType(value);\n      const children = generateChildren(childInfo, childPath);\n\n      return {\n        id: childPath.toString(),\n        name: key,\n        title: key,\n        subtitle: formatValue(childInfo),\n        icon: iconForType(childInfo),\n        children,\n      };\n    });\n  }\n\n  return [];\n}\n\nexport function generateNodesToPath(\n  json: unknown,\n  path: string\n): Array<ColumnViewNode> {\n  const heroPath = new JSONHeroPath(path);\n\n  const currentPathComponents: Array<PathComponent> = [];\n\n  const nodes: Array<ColumnViewNode> = [];\n\n  for (const component of heroPath.components) {\n    currentPathComponents.push(component);\n\n    const currentPath = new JSONHeroPath(currentPathComponents);\n\n    const info = inferType(currentPath.first(json));\n\n    const componentName = component.toString();\n\n    nodes.push({\n      name: componentName,\n      title: componentName === \"$\" ? \"root\" : componentName,\n      id: currentPath.toString(),\n      icon: iconForType(info),\n      children: [],\n    });\n  }\n\n  return nodes;\n}\n\nexport function firstChildToDescendant(\n  ancestor: JSONHeroPath,\n  descendant: JSONHeroPath\n): string | undefined {\n  const ancestorPathComponents = ancestor.components.map((component) =>\n    component.toString()\n  );\n  const descendantPathComponents = descendant.components.map((component) =>\n    component.toString()\n  );\n\n  const pathComponents = [];\n\n  for (let index = 0; index < descendantPathComponents.length; index++) {\n    const descendantPathComponent = descendantPathComponents[index];\n\n    if (ancestorPathComponents.length >= index) {\n      pathComponents.push(descendantPathComponent);\n    }\n  }\n\n  return pathComponents.join(\".\");\n}\n\nexport function pathToDescendant(\n  ancestor: string,\n  descendant: string\n): string | undefined {\n  const ancestorPath = new JSONHeroPath(ancestor);\n  const descendantPath = new JSONHeroPath(descendant);\n\n  const ancestorPathComponents = ancestorPath.components.map((component) =>\n    component.toString()\n  );\n  const descendantPathComponents = descendantPath.components.map((component) =>\n    component.toString()\n  );\n\n  const pathComponents = [];\n\n  for (let index = 0; index < descendantPathComponents.length; index++) {\n    const descendantPathComponent = descendantPathComponents[index];\n\n    if (ancestorPathComponents.length <= index) {\n      pathComponents.push(descendantPathComponent);\n    }\n  }\n\n  return pathComponents.join(\".\");\n}\n\n//Given the previous path and where the selection is, we calculate the new path\n//This allows us to keep the deepest item possible still visible whilst navigating around\nexport function calculateStablePath(\n  previousPathString: string,\n  selectionPathString: string,\n  json: unknown\n): string {\n  const previousPath = new JSONHeroPath(previousPathString);\n  const selectionPath = new JSONHeroPath(selectionPathString);\n\n  //if we are selecting at the right edge then the selection determines the path\n  if (selectionPath.components.length >= previousPath.components.length) {\n    return selectionPathString;\n  }\n\n  //if the selection is the same as the path from the start up to selection end then we leave the path as is\n  const previousPathWithSelectionLength = new JSONHeroPath(\n    previousPath.components.slice(0, selectionPath.components.length)\n  );\n  if (selectionPathString === previousPathWithSelectionLength.toString()) {\n    return previousPathString;\n  }\n\n  //from the start we add the selection components until they don't match the previous path (this should be until the last one)\n  let newComponents: PathComponent[] = [];\n  for (let index = 0; index < selectionPath.components.length; index++) {\n    const selectionComponent = selectionPath.components[index];\n    const previousComponent = previousPath.components[index];\n\n    //if they're different we need to bail and try build an alternative path\n    if (selectionComponent.toString() !== previousComponent.toString()) {\n      break;\n    }\n\n    newComponents.push(selectionComponent);\n  }\n\n  //we substitute all the remaining elements from the selection into the previousPath\n  const remainingSelectionComponents = selectionPath.components.slice(\n    newComponents.length\n  );\n  const remainingPreviousPathComponents = previousPath.components.slice(\n    selectionPath.components.length\n  );\n  const updatedPathComponents = [\n    ...newComponents,\n    ...remainingSelectionComponents,\n    ...remainingPreviousPathComponents,\n  ];\n  const updatedPath = new JSONHeroPath(updatedPathComponents);\n\n  const jsonAtUpdatedPath = updatedPath.first(json);\n\n  //not a match then we return the selection path\n  if (!jsonAtUpdatedPath) {\n    return selectionPathString;\n  } else {\n    return updatedPath.toString();\n  }\n}\n"
  },
  {
    "path": "app/utilities/nullable.ts",
    "content": "import { JSONHeroPath } from \"@jsonhero/path\";\n\nexport function isNullable(relatedPaths: string[], json: unknown): boolean {\n  return relatedPaths.some((path) => {\n    const heroPath = new JSONHeroPath(path);\n\n    const value = heroPath.first(json);\n\n    return value == null;\n  });\n}\n"
  },
  {
    "path": "app/utilities/relatedValues.ts",
    "content": "import { inferType } from \"@jsonhero/json-infer-types\";\nimport { JSONHeroPath } from \"@jsonhero/path\";\nimport { groupBy, sortBy } from \"lodash-es\";\n\nexport type RelatedValuesGroup = {\n  value: string;\n  paths: Array<string>;\n};\n\nexport function calculateRelatedValuesGroups(\n  path: string,\n  json: unknown\n): Array<RelatedValuesGroup> {\n  const relatedPaths = getRelatedPathsAtPath(path, json);\n  return groupRelatedValues(relatedPaths, json);\n}\n\nexport function groupRelatedValues(\n  relatedPaths: Array<string>,\n  json: unknown\n): Array<RelatedValuesGroup> {\n  const groupedByValue = groupBy(relatedPaths, (path) => {\n    const heroPath = new JSONHeroPath(path);\n\n    const value = heroPath.first(json);\n\n    if (typeof value === \"undefined\") {\n      return \"undefined\";\n    } else if (value == null) {\n      return \"null\";\n    } else if (Array.isArray(value)) {\n      return `Array(${value.length})`;\n    } else if (typeof value === \"object\") {\n      return \"{...}\";\n    } else {\n      return value.toString();\n    }\n  });\n\n  const unsortedResult = Object.entries(groupedByValue).map(\n    ([value, paths]) => {\n      return {\n        value,\n        paths: sortBy(paths),\n      };\n    }\n  );\n\n  return sortBy(unsortedResult, (group) => -group.paths.length);\n}\n\nexport function getRelatedPathsAtPath(\n  path: string,\n  json: unknown,\n  relatedPaths: Set<string> = new Set<string>()\n): Array<string> {\n  const initialPath = new JSONHeroPath(path);\n  const pathDepth = initialPath.components.length;\n\n  for (let index = 0; index < pathDepth; index++) {\n    const pathToComponent = new JSONHeroPath(\n      initialPath.components.slice(0, index + 1)\n    );\n\n    const value = pathToComponent.first(json);\n\n    if (typeof value === \"undefined\") {\n      continue;\n    }\n\n    //optimisation: we only want to call inferType on non-array types\n    if (typeof value !== \"object\" || !Array.isArray(value)) continue;\n\n    const inferredType = inferType(value);\n\n    if (inferredType.name !== \"array\") continue;\n\n    if (index + 1 === pathDepth) continue;\n\n    for (\n      let childIndex = 0;\n      childIndex < inferredType.value.length;\n      childIndex++\n    ) {\n      const relatedPath = initialPath.replaceComponent(\n        index + 1,\n        `${childIndex}`\n      );\n\n      if (relatedPaths.has(relatedPath.toString())) continue;\n\n      const parentValue = relatedPath.parent?.first(json);\n\n      if (!parentValue) continue;\n\n      relatedPaths.add(relatedPath.toString());\n\n      getRelatedPathsAtPath(relatedPath.toString(), json, relatedPaths);\n    }\n  }\n\n  return Array.from(relatedPaths);\n}\n"
  },
  {
    "path": "app/utilities/safeFetch.ts",
    "content": "export default function safeFetch(\n  url: string,\n  options: RequestInit = {}\n): Promise<Response> {\n  return fetch(url, {\n    ...options,\n    headers: {\n      \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36\",\n      ...options.headers,\n    },\n  });\n}\n"
  },
  {
    "path": "app/utilities/search.ts",
    "content": "import { Array, Dict } from \"@swan-io/boxed\";\nimport { groupBy, uniq } from \"lodash-es\";\n\ntype StringSlice = {\n  isMatch: boolean;\n  slice: string;\n};\n\n// getStringSlices should scope to the largest match\n// For example, if the windowSize is 56 and the stringValue is \"This is a very long string and the largest matched range is outside of the window, so we should try and get only slices of the string that focus on the largest match\"\n// and the matches are [{ start: 10, end: 16 }, { start: 80, end: 91 }, { start: 100, end: 106 }]\n// then we should return slices:\n// [\n//   { isMatch: false, slice: \"…\" }\n//   { isMatch: false, slice: \"the string,\" },\n//   { isMatch: true, slice: \", we should\" },\n//   { isMatch: false, slice: \"d only retu\" },\n//   { isMatch: true, slice: \"urn sl\" },\n//   { isMatch: false, slice: \"lices that are within\" },\n//   { isMatch: false, slice: \"…\" },\n//\n// If stringValue length is less than the windowSize, then we should return all the string slices of the string\nexport function getStringSlices(\n  stringValue: string,\n  matchingIndices: ReadonlyArray<{ start: number; end: number }>,\n  windowSize: number\n): Array<StringSlice> {\n  const slices: StringSlice[] = [];\n\n  const addSlice = (isMatch: boolean, slice: string) => {\n    if (slice.length > 0) {\n      slices.push({ isMatch, slice });\n    }\n  };\n\n  const addEllipsis = () => {\n    addSlice(false, \"…\");\n  };\n\n  const calculateWindow = (): { start: number; end: number } => {\n    if (stringValue.length <= windowSize) {\n      return { start: 0, end: stringValue.length };\n    }\n\n    const largestMatch = matchingIndices.reduce(\n      (largestMatch, match) => {\n        if (match.end - match.start > largestMatch.end - largestMatch.start) {\n          return match;\n        }\n\n        return largestMatch;\n      },\n      { start: 0, end: 0 }\n    );\n\n    const largestMatchLength = largestMatch.end - largestMatch.start;\n\n    const start =\n      largestMatch.start - Math.floor(windowSize / 2 - largestMatchLength / 2);\n    const end =\n      largestMatch.end + Math.floor(windowSize / 2 - largestMatchLength / 2);\n\n    return {\n      start: Math.max(start, 0),\n      end: Math.min(end, stringValue.length),\n    };\n  };\n\n  const window = calculateWindow();\n\n  let currentIndex = window.start;\n\n  if (window.start > 0) {\n    addEllipsis();\n  }\n\n  for (const { start, end } of matchingIndices) {\n    if (start < window.start && end < window.start) {\n      continue;\n    } else if (start > window.end) {\n      continue;\n    } else if (start < window.start && end > window.start) {\n      addSlice(true, stringValue.slice(window.start, end));\n\n      currentIndex = end;\n    } else if (start >= window.start && end <= window.end) {\n      if (start > 0) {\n        addSlice(false, stringValue.slice(currentIndex, start));\n      }\n      addSlice(true, stringValue.slice(start, end));\n      currentIndex = end;\n    }\n  }\n\n  addSlice(false, stringValue.slice(currentIndex, window.end).trimEnd());\n\n  if (window.end < stringValue.length) {\n    addEllipsis();\n  }\n\n  return slices;\n}\n\ntype EllispisSlice = {\n  type: \"ellipsis\";\n};\n\ntype ComponentSlice = {\n  type: \"component\";\n  componentIndex: number;\n  slice: StringSlice;\n};\n\ntype JoinSlice = {\n  type: \"join\";\n};\n\nexport type PathSlice = EllispisSlice | ComponentSlice | JoinSlice;\n\n// getComponentSlices returns slices that are either ellipsis or component\n// and the component slices are the slices of the component string that, depending on the matchingIndices\n//\n// If the \"weight\" of the path is more than the window size, then we need to \"hide\" some of the component strings behind an ellipsis\n// But the hidden components shouldn't be ones that are matched, sorted by the largest match first.\n//\n// The \"weight\" of the path is calculated by summing the length of the component strings + (8 * the number of components)\n//\n// Ellipsis is a special case where the weight is 2\n//\n// Example:\n// path = records.0.users.9.addresses.0.street_address.street_name\n// maxWeight = 60\n// matchingIndices = [ { start: 0, end: 2 }, { start: 11, end: 15 }, { start: 30, end: 36 }, { start: 45, end: 51 } ]\n//\n// Weight Calculation:\n//  records = 7\n//  . = 8\n//  0 = 1\n//  . = 8\n//  users = 5\n//  . = 8\n//  9 = 1\n//  . = 8\n//  addresses = 9\n//  . = 8\n//  0 = 1\n//  . = 8\n//  street_address = 14\n//  . = 8\n//  street_name = 11\n//\n// Total Weight: 7 + 8 + 1 + 8 + 5 + 8 + 1 + 8 + 9 + 8 + 1 + 8 + 14 + 8 + 11 = 105\n//\n//\n// To get below the maxWeight, we need to hide the components that are not the largest match\n//\n// Using the example from above, the result should be:\n// records = 7\n// . = 8\n// … = 2\n// . = 8\n//  street_address = 14\n//  . = 8\n//  street_name = 11\n//\n// Total Weight: 7 + 8 + 2 + 8 + 14 + 8 + 11 = 58\n//\n// So the result from getComponentSlices for the above example should be:\n// [\n//   { type: \"component\", slice: { isMatch: true, slice: \"re\" } },\n//   { type: \"component\", slice: { isMatch: false, slice: \"cords\" } },\n//   { type: \"join\" },\n//   { type: \"ellipsis\" },\n//   { type: \"join\" },\n//   { type: \"component\", slice: { isMatch: true, slice: \"street\" } },\n//   { type: \"component\", slice: { isMatch: false, slice: \"_address\" } },\n//   { type: \"join\" },\n//   { type: \"component\", slice: { isMatch: true, slice: \"street\" } },\n//   { type: \"component\", slice: { isMatch: false, slice: \"_name\" } },\n// ]\n//\n// Some rules:\n// 1. We never have more than one ellipsis\n// 2. We never hide the first component behind an ellipsis\n// 3. We try to get the final weight to be as close to the maxWeight as possible\n//\nexport function getComponentSlices(\n  path: string,\n  matchingIndices: ReadonlyArray<{ start: number; end: number }>,\n  maxWeight: number\n): Array<PathSlice> {\n  const calculateWeight = (pathSlices: PathSlice[]): number => {\n    let weight = 0;\n\n    for (const slice of pathSlices) {\n      weight += calculateSliceWeight(slice);\n    }\n\n    return weight;\n  };\n\n  const calculateSliceWeight = (slice: PathSlice): number => {\n    if (slice.type === \"component\") {\n      return slice.slice.slice.length;\n    } else if (slice.type === \"ellipsis\") {\n      return 2;\n    } else {\n      return 8;\n    }\n  };\n\n  const calculateLongestMatch = (componentSlices: ComponentSlice[]): number => {\n    return componentSlices.reduce(\n      (longestMatch, slice) =>\n        slice.slice.isMatch\n          ? Math.max(longestMatch, slice.slice.slice.length)\n          : longestMatch,\n      0\n    );\n  };\n\n  const addEllipsisToSlices = (\n    slices: PathSlice[],\n    mostImportantComponentIndex: number\n  ): PathSlice[] => {\n    // This should take an array of slices like this:\n    // [\n    //   { type: \"component\", slice: { isMatch: true, slice: \"re\" } },\n    //   { type: \"component\", slice: { isMatch: false, slice: \"cords\" } },\n    //   { type: \"join\" },\n    //   { type: \"ellipsis\" },\n    //   { type: \"join\" },\n    //   { type: \"ellipsis\" },\n    //   { type: \"ellipsis\" },\n    //   { type: \"join\" },\n    //   { type: \"component\", slice: { isMatch: true, slice: \"street\" } },\n    //   { type: \"component\", slice: { isMatch: false, slice: \"_address\" } },\n    // ]\n    //\n    //\n    // And should return this:\n    // [\n    //   { type: \"component\", slice: { isMatch: true, slice: \"re\" } },\n    //   { type: \"component\", slice: { isMatch: false, slice: \"cords\" } },\n    //   { type: \"join\" },\n    //   { type: \"ellipsis\" },\n    //   { type: \"join\" },\n    //   { type: \"component\", slice: { isMatch: true, slice: \"street\" } },\n    //   { type: \"component\", slice: { isMatch: false, slice: \"_address\" } },\n    // ]\n    const combineAdjacentEllipsis = (toCombine: PathSlice[]): PathSlice[] => {\n      const combined: PathSlice[] = [];\n      let inEllipsis = false;\n\n      for (let i = 0; i < toCombine.length; i++) {\n        const slice = toCombine[i];\n\n        if (slice.type === \"ellipsis\") {\n          if (!inEllipsis) {\n            inEllipsis = true;\n            combined.push(slice);\n          }\n        }\n\n        if (slice.type === \"join\") {\n          if (!inEllipsis) {\n            combined.push(slice);\n          }\n        }\n\n        if (slice.type === \"component\") {\n          if (inEllipsis) {\n            combined.push({ type: \"join\" });\n            inEllipsis = false;\n          }\n          combined.push(slice);\n        }\n      }\n\n      return combined;\n    };\n\n    const replaceComponentIndexWithEllipsis = (\n      ellipsisComponentIndex: number\n    ): PathSlice[] => {\n      const ellipsisSliceIndices = slices\n        .map((slice, index) => [slice, index] as [PathSlice, number])\n        .filter(\n          ([slice, index]) =>\n            slice.type === \"component\" &&\n            slice.componentIndex === ellipsisComponentIndex\n        )\n        .map(([, index]) => index);\n\n      const newEllipsis = slices.map((slice, index) => {\n        if (ellipsisSliceIndices.includes(index)) {\n          return {\n            type: \"ellipsis\",\n          };\n        } else {\n          return slice;\n        }\n      }) as PathSlice[];\n\n      return combineAdjacentEllipsis(newEllipsis);\n    };\n\n    const componentSlices = Array.keepMap(slices, (slice) =>\n      slice.type === \"component\" ? slice : null\n    );\n\n    const componentIndexes = uniq(\n      componentSlices.map((slice) => slice.componentIndex)\n    );\n\n    const ellipsisIndex = slices.findIndex(\n      (slice) => slice.type === \"ellipsis\"\n    );\n\n    if (ellipsisIndex === -1) {\n      let ellipsisComponentIndex = 0;\n      // There are no ellipsis yet, so we need to figure out where to put the first one\n      if (mostImportantComponentIndex === 0) {\n        ellipsisComponentIndex = componentIndexes[componentIndexes.length - 2];\n      } else if (mostImportantComponentIndex === componentIndexes.length - 1) {\n        ellipsisComponentIndex = componentIndexes[1];\n      } else {\n        const halfWay = Math.floor(componentIndexes.length / 2);\n\n        if (mostImportantComponentIndex < halfWay) {\n          ellipsisComponentIndex = componentIndexes[halfWay + 1];\n        }\n\n        if (mostImportantComponentIndex > halfWay) {\n          ellipsisComponentIndex = componentIndexes[halfWay - 1];\n        }\n\n        if (mostImportantComponentIndex === halfWay) {\n          ellipsisComponentIndex = componentIndexes[1];\n        }\n      }\n\n      return replaceComponentIndexWithEllipsis(ellipsisComponentIndex);\n    } else {\n      // Add to the existing ellipsis\n      // Get nearest component index to the ellipsis, before and after\n      const nearestBefore = Array.keepMap(\n        slices.slice(0, ellipsisIndex).reverse(),\n        (slice) => (slice.type === \"component\" ? slice : null)\n      )[0];\n\n      const nearestAfter = Array.keepMap(\n        slices.slice(ellipsisIndex + 1),\n        (slice) => (slice.type === \"component\" ? slice : null)\n      )[0];\n\n      if (\n        nearestBefore.componentIndex !== 0 &&\n        nearestBefore.componentIndex !== mostImportantComponentIndex\n      ) {\n        return replaceComponentIndexWithEllipsis(nearestBefore.componentIndex);\n      } else if (\n        nearestAfter.componentIndex !== 0 &&\n        nearestAfter.componentIndex !== mostImportantComponentIndex\n      ) {\n        return replaceComponentIndexWithEllipsis(nearestAfter.componentIndex);\n      }\n    }\n\n    return slices;\n  };\n\n  let slices = createComponentSlices(path, matchingIndices);\n\n  let weight = calculateWeight(slices);\n\n  while (weight > maxWeight) {\n    const componentSlices = Array.keepMap(slices, (slice) =>\n      slice.type === \"component\" ? slice : null\n    );\n\n    const groupByComponentIndex = groupBy(\n      componentSlices,\n      (slice) => slice.componentIndex\n    );\n\n    const sortedByLongestMatch = Dict.entries(groupByComponentIndex).sort(\n      ([, componentSlicesA], [, componentSlicesB]) => {\n        if (\n          calculateLongestMatch(componentSlicesA) >\n          calculateLongestMatch(componentSlicesB)\n        ) {\n          return -1;\n        }\n\n        if (\n          calculateLongestMatch(componentSlicesB) >\n          calculateLongestMatch(componentSlicesA)\n        ) {\n          return 1;\n        }\n\n        return 0;\n      }\n    );\n\n    const mostImportantComponentIndex = Number(sortedByLongestMatch[0][0]);\n\n    slices = addEllipsisToSlices(slices, mostImportantComponentIndex);\n\n    const newWeight = calculateWeight(slices);\n\n    // Just in case we can't shrink the weight any further\n    if (newWeight === weight) {\n      break;\n    }\n\n    weight = newWeight;\n  }\n\n  return slices;\n}\n\nexport function createComponentSlices(\n  path: string,\n  matchingIndices: ReadonlyArray<{ start: number; end: number }>\n): Array<PathSlice> {\n  const slices: PathSlice[] = [];\n\n  const addComponent = (slice: StringSlice, componentIndex: number) => {\n    slices.push({ type: \"component\", componentIndex, slice });\n  };\n\n  const addJoin = () => {\n    slices.push({ type: \"join\" });\n  };\n\n  const addEllipsis = () => {\n    slices.push({ type: \"ellipsis\" });\n  };\n\n  const components = path.split(\".\");\n\n  let currentIndex = 0;\n  let currentComponentIndex = 0;\n\n  for (const component of components) {\n    if (currentComponentIndex !== 0) {\n      // Adds the \".\"\n      currentIndex += 1;\n    }\n\n    const endIndex = currentIndex + component.length;\n\n    // Example matchingIndices = [{ start: 0, end: 2 }, { start: 6, end: 11 }, { start: 12, end: 21 }]\n    // currentIndex = 7\n    // endIndex = 7 + 6 = 13\n    const intersectingMatches = matchingIndices\n      .filter(\n        ({ start, end }) =>\n          (currentIndex >= start && currentIndex <= end) ||\n          (endIndex >= start && endIndex <= end) ||\n          (start >= currentIndex && end <= endIndex)\n      )\n      .map(({ start, end }) => ({\n        start: Math.max(start - currentIndex, 0),\n        end: end - currentIndex,\n      }));\n\n    const stringSlices = getStringSlices(\n      component,\n      intersectingMatches,\n      component.length + 1\n    );\n\n    for (const stringSlice of stringSlices) {\n      addComponent(stringSlice, currentComponentIndex);\n    }\n\n    if (currentComponentIndex + 1 < components.length) {\n      addJoin();\n    }\n\n    currentComponentIndex += 1;\n    currentIndex = endIndex;\n  }\n\n  return slices;\n}\n"
  },
  {
    "path": "app/utilities/stableJson.ts",
    "content": "export function stableJson(json: unknown, keyOrder: string[] = []): unknown {\n  if (\n    Array.isArray(json) &&\n    json.length > 0 &&\n    json.every((c) => typeof c === \"object\" && c !== null)\n  ) {\n    const keyOrder = Object.keys(json[0]);\n    return json.map((c) => stableJson(c, keyOrder));\n  }\n\n  if (Array.isArray(json)) {\n    return json.map((c) => stableJson(c));\n  }\n\n  if (typeof json === \"object\" && json !== null && keyOrder.length > 0) {\n    const keys = Object.keys(json);\n    const sortedKeys = keys.sort((a, b) => {\n      const aIndex = keyOrder.indexOf(a);\n      const bIndex = keyOrder.indexOf(b);\n\n      if (aIndex === -1 || bIndex === -1) {\n        return 0;\n      }\n\n      return aIndex - bIndex;\n    });\n    const result = {} as Record<string, unknown>;\n    for (const key of sortedKeys) {\n      result[key] = stableJson((json as Record<string, unknown>)[key]);\n    }\n    return result;\n  }\n\n  if (typeof json === \"object\" && json !== null) {\n    const result = {} as Record<string, unknown>;\n    for (const key of Object.keys(json)) {\n      result[key] = stableJson((json as Record<string, unknown>)[key]);\n    }\n    return result;\n  }\n\n  return json;\n}\n"
  },
  {
    "path": "app/utilities/xml/__test__/convertXmlToJsonString.test.ts",
    "content": "import * as fs from \"fs\";\nimport { join } from \"path\";\nimport convertFromRawXml from \"../convertFromRawXml\";\n\nconst xmlString = fs.readFileSync(join(__dirname, \"./xml.txt\"), \"utf8\");\n\ndescribe(\"convertFromRawXml\", () => {\n  test(\"Returns a string\", () => {\n    expect(typeof convertFromRawXml(\"<xml></xml>\")).toBe(\"string\");\n    expect(typeof convertFromRawXml(xmlString)).toBe(\"string\");\n  });\n\n  test(\"Returns a string that is valid JSON string\", () => {\n    expect(() => JSON.parse(convertFromRawXml(\"<xml></xml>\"))).not.toThrow();\n    expect(() => JSON.parse(convertFromRawXml(xmlString))).not.toThrow();\n  });\n\n  test(\"Throw error if invalid XML\", () => {\n    expect(() => convertFromRawXml(\"<xml></xml\")).toThrow();\n    expect(() => convertFromRawXml(\"\")).toThrow();\n    expect(() => convertFromRawXml(\"lol\")).toThrow();\n  });\n});\n"
  },
  {
    "path": "app/utilities/xml/__test__/isXML.test.ts",
    "content": "import * as fs from \"fs\";\nimport { join } from \"path\";\nimport isXML from \"../isXML\";\n\nconst xmlString = fs.readFileSync(join(__dirname, \"./xml.txt\"), \"utf8\");\n\ndescribe(\"isXML\", () => {\n  test(\"returns true for valid XML\", () => {\n    expect(isXML(\"<xml></xml>\")).toBe(true);\n    expect(isXML(\"<whatever>Content</whatever>\")).toBe(true);\n    expect(isXML(xmlString)).toBe(true);\n  });\n  test(\"returns false for invalid XML\", () => {\n    expect(isXML(\"<xml></xml\")).toBe(false);\n    expect(isXML(\"lol\")).toBe(false);\n    expect(isXML(\"\")).toBe(false);\n  });\n});\n"
  },
  {
    "path": "app/utilities/xml/__test__/xml.txt",
    "content": "<!-- <?xml version = \"1.0\"?>\n<!DOCTYPE ORDERFILE [\n<!ELEMENT ORDERFILE (CUSTOMER)*>\n<!ELEMENT CUSTOMER (NAME, DATE, ORDERS)>\n<!ELEMENT NAME (LAST-NAME, FIRST-NAME)>\n<!ELEMENT LAST-NAME (#PCDATA)>\n<!ELEMENT FIRST-NAME (#PCDATA)>\n<!ELEMENT DATE (#PCDATA)>\n<!ELEMENT ORDERS (ITEM)*>\n<!ELEMENT ITEM (PRODUCT, NUMBER, (PRICE | CHARGEACCT | SAMPLE))>\n<!ELEMENT PRODUCT (#PCDATA)>\n<!ELEMENT NUMBER (#PCDATA)>\n<!ELEMENT PRICE (#PCDATA)>\n<!ELEMENT CHARGEACCT (#PCDATA)>\n<!ELEMENT SAMPLE (#PCDATA)>\n]> -->\n<ORDERFILE>\n  <CUSTOMER>\n    <NAME gender=\"female\">\n      <LAST-NAME>Smith</LAST-NAME>\n      <FIRST-NAME>Sam</FIRST-NAME>\n    </NAME>\n    <DATE>October 15, 2001</DATE>\n    <ORDERS>\n      <ITEM>\n        <PRODUCT>Tomatoes</PRODUCT>\n        <NUMBER>8</NUMBER>\n        <PRICE>1.25</PRICE>\n      </ITEM>\n      <ITEM>\n        <PRODUCT>Apples</PRODUCT>\n        <NUMBER>12</NUMBER>\n        <PRICE>2.50</PRICE>\n      </ITEM>\n      <ITEM>\n        <PRODUCT>Bananas</PRODUCT>\n        <NUMBER>6</NUMBER>\n        <PRICE>.50</PRICE>\n      </ITEM>\n    </ORDERS>\n  </CUSTOMER>\n  <CUSTOMER gender=\"male\">\n    <NAME>\n      <LAST-NAME>Snead</LAST-NAME>\n      <FIRST-NAME>Todd</FIRST-NAME>\n    </NAME>\n    <DATE>October 17, 2001</DATE>\n    <ORDERS>\n      <ITEM>\n        <PRODUCT>Slicer/Dicer</PRODUCT>\n        <NUMBER>1</NUMBER>\n        <CHARGEACCT>1234-5678-3456-7890</CHARGEACCT>\n      </ITEM>\n    </ORDERS>\n  </CUSTOMER>\n</ORDERFILE>"
  },
  {
    "path": "app/utilities/xml/convertFromRawXml.ts",
    "content": "import { DOMParser } from \"@xmldom/xmldom\";\n\nexport type SerializedXMLObject = {\n  [key: string]: [] | string | {} | undefined;\n  $attributes?: { [key: string]: any };\n  $values?: [] | { [key: string]: any };\n};\n\nconst getCleanXmlString = (xmlString: string): string => {\n  const cleanXmlString = xmlString\n    .replace(/(\\r\\n|\\n|\\r)/gm, \"\") // remove line breaks\n    .replace(/>\\s+</g, \"><\"); // remove all whitespaces between tags\n  return cleanXmlString;\n};\n\nconst serializeXml = (\n  node: ChildNode & { attributes?: NamedNodeMap }\n): SerializedXMLObject | string | undefined => {\n  const { nodeName, nodeType, nodeValue } = node;\n\n  // text\n  if (nodeType === 3) {\n    return nodeValue || undefined;\n  }\n\n  // comment, ignore\n  if (nodeType === 8) {\n    return undefined;\n  }\n\n  const children = Array.from(node.childNodes).map((child) =>\n    serializeXml(child)\n  );\n\n  const attributes =\n    node.attributes &&\n    Array.from(node.attributes).reduce(\n      (acc: {}, attr: any) => ({ ...acc, [attr.name]: attr.value }),\n      {}\n    );\n\n  let childObject: any = {};\n\n  if (children.length === 1 && typeof children[0] === \"string\") {\n    childObject[nodeName] = children[0];\n  } else {\n    childObject[nodeName] = {};\n\n    // childenUniqueKeys check if children should be processed as array\n    // or should be added as properties of parent object.\n    // e.g: In [{ name: 'foo' }, { name: 'bar' }],\n    // children bear the same \"name\" key, so parent object will look like:\n    //\n    // parent: {\n    //   $values: [\n    //     { name: 'foo' },\n    //     { name: 'bar' }\n    //   ],\n    //   $attributes: { ... }\n    // }\n    //\n    // In [{ name: 'foo' }, { age: 10 }],\n    // children have different keys and will be merged into parent object:\n    // parent: { name: 'foo', age: 10 }\n    const childenUniqueKeys = new Set(\n      children.map((child: any) => Object.keys(child)[0])\n    );\n\n    if (childenUniqueKeys.size === children.length) {\n      childObject[nodeName] = children.reduce(\n        (acc: {}, child: any) => ({ ...acc, ...child }),\n        {}\n      );\n    } else {\n      childObject[nodeName].$values = children;\n    }\n  }\n\n  if (attributes && Object.keys(attributes).length) {\n    childObject[nodeName].$attributes = attributes;\n  }\n\n  return childObject;\n};\n\nexport default function convertFromRawXml(xmlString: string): string {\n  const cleanXmlString = getCleanXmlString(xmlString);\n\n  // Read comment in isXML.ts for why we need to handle error this way\n  const xmlDoc = new DOMParser({\n    errorHandler: {\n      warning: () => {},\n      error: () => {\n        throw new Error(\"Invalid XML\");\n      },\n      fatalError: () => {\n        throw new Error(\"Invalid XML\");\n      },\n    },\n  }).parseFromString(cleanXmlString, \"application/xml\");\n\n  // This line is necessary because xmldom does not throw an error\n  // if we pass it a plain string.\n  if (!xmlDoc?.documentElement) throw new Error(\"Invalid XML\");\n\n  const nodes = Array.from(xmlDoc.childNodes);\n  const serialized = nodes.map((node) => {\n    return serializeXml(node);\n  });\n\n  return JSON.stringify(serialized);\n}\n"
  },
  {
    "path": "app/utilities/xml/createFromRawXml.ts",
    "content": "import {\n  createFromRawJson,\n  CreateJsonOptions,\n  JSONDocument,\n} from \"~/jsonDoc.server\";\nimport convertFromRawXml from \"./convertFromRawXml\";\n\nexport default async function createFromRawXml(\n  filename: string,\n  contents: string,\n  options?: CreateJsonOptions\n): Promise<JSONDocument> {\n  const jsonString: string = convertFromRawXml(contents);\n  return createFromRawJson(filename, jsonString, options);\n}"
  },
  {
    "path": "app/utilities/xml/isXML.ts",
    "content": "import { DOMParser } from \"@xmldom/xmldom\";\n\nexport default function isXML(possibleXml: string): boolean {\n  let isValid = true;\n\n  // https://www.npmjs.com/package/xmldom\n  // xmldom handles invalid XML gracefully, so we need to check for errors\n  // in this way, rather than relying on the return value of parseFromString\n  // as recommended in this documentation::\n  // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#error_handling\n\n  const xmlDoc = new DOMParser({\n    errorHandler: {\n      warning: () => {},\n      error: () => {\n        isValid = false;\n      },\n      fatalError: () => {\n        isValid = false;\n      },\n    },\n  }).parseFromString(possibleXml, \"application/xml\");\n\n  // This line is necessary because xmldom does not throw an error\n  // if we pass it a plain string.\n  if (!xmlDoc?.documentElement) isValid = false;\n\n  return isValid;\n}\n"
  },
  {
    "path": "examples/owenWilsonWows.json",
    "content": "[\n  {\n    \"movie\": \"Cars 3\",\n    \"year\": 2017,\n    \"release_date\": \"2017-05-23\",\n    \"director\": \"Brian Fee\",\n    \"character\": \"Lighting McQueen\",\n    \"movie_duration\": \"01:42:25\",\n    \"timestamp\": \"00:25:10\",\n    \"full_line\": \"Oh, hey, Mr. Sterling. Wow.\",\n    \"current_wow_in_movie\": 3,\n    \"total_wows_in_movie\": 10,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/43fOBsgY8iOJL0dijvFnfl/ea361efc5131a859c173ab5dd3fdfe1e/Cars_3_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/MwAHRe21gYt7qgtQqQSgi/20188401d4ae548db5c4f861fad96f49/Cars_3_Wow_3_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7dL6LdSOmZ2mbXgdi4KeZl/6fe6efef423d28e5704637658a3650ec/Cars_3_Wow_3_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7uciNXhLeVXBByZu8ipAr4/5bcae43dde19fa0bb249e9318953ba09/Cars_3_Wow_3_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/KL91bYDqfWVeK6NdaXh1O/f1baca7b2988a2e5853256be5f9e2c89/Cars_3_Wow_3_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/6PlYbs7cO3iXEEM1szcfHZ/bf10fba861c8e0d2c20ac4714974bde1/Cars_3_Wow_3.mp3\"\n  },\n  {\n    \"movie\": \"The Internship\",\n    \"year\": 2013,\n    \"release_date\": \"2013-05-29\",\n    \"director\": \"Shawn Levy\",\n    \"character\": \"Nick Campbell\",\n    \"movie_duration\": \"02:05:02\",\n    \"timestamp\": \"00:38:59\",\n    \"full_line\": \"Wow! Seven projects.\",\n    \"current_wow_in_movie\": 1,\n    \"total_wows_in_movie\": 5,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/6XhmTpc1PoiTf0q1nDYMyv/594947f72b4dba7f3c24938a680dd603/The_Internship_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/11VKtDgrvqvU4atLRF9QV9/f1f1ae91486a001e41fb3f66821f1e62/The_Internship_Wow_1_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6uDteZpujqfebwhVAXoFkm/768bbb0a63dcef265b0040b54e417483/The_Internship_Wow_1_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/4JTqjEo1kTWtALpSdE9pVs/f9242e24e89c9bd32aa3f5212eca08e9/The_Internship_Wow_1_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/5VGJ7XEkBhxEbLMuDR4AIC/e5b80d57316dbb910cd72685f72b8f2b/The_Internship_Wow_1_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/2vwdiSubw2iEcsl1yCUk0A/3cb1ba763c12fd7711823619b4c3d904/The_Internship_Wow_1.mp3\"\n  },\n  {\n    \"movie\": \"Cars 3\",\n    \"year\": 2017,\n    \"release_date\": \"2017-05-23\",\n    \"director\": \"Brian Fee\",\n    \"character\": \"Lighting McQueen\",\n    \"movie_duration\": \"01:42:25\",\n    \"timestamp\": \"01:03:08\",\n    \"full_line\": \"Wow. You don't mince words around here, do you?\",\n    \"current_wow_in_movie\": 10,\n    \"total_wows_in_movie\": 10,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/43fOBsgY8iOJL0dijvFnfl/ea361efc5131a859c173ab5dd3fdfe1e/Cars_3_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/3KJZ435sr6Y5k00AnZdOJ2/2aadc7fbc687b38c3f1536a9efc16cdb/Cars_3_Wow_10_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/5vQtn6ny9J79PNG16jsJMR/1b4804366cfb9206d70472ab9b7f37e6/Cars_3_Wow_10_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6kIDoftFyBJOgvCBe5Jr5k/51be6dcd0a40aff052a4215258f6f2cb/Cars_3_Wow_10_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/923BnYGWVuiPpWeM7e6wv/db39600a6bd7c6a807e95d397d129dd4/Cars_3_Wow_10_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/3iZaUMMLnxCH1fIqcXpYZ0/78e6ee967cae46e64a6f65c6f451115a/Cars_3_Wow_10.mp3\"\n  },\n  {\n    \"movie\": \"Night at the Museum: Secret of the Tomb\",\n    \"year\": 2014,\n    \"release_date\": \"2014-12-11\",\n    \"director\": \"Shawn Levy\",\n    \"character\": \"Jedediah Smith\",\n    \"movie_duration\": \"01:37:48\",\n    \"timestamp\": \"00:39:36\",\n    \"full_line\": \"Wow. We're a long way from home, boy.\",\n    \"current_wow_in_movie\": 1,\n    \"total_wows_in_movie\": 1,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/4cwNnRAOKM1HuAtlH6dscl/52762736d4815c2390619a4009d66d93/Night_at_the_Museum_Secret_of_the_Tomb_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/uHF5B0MFoK8H0S40Sqld9/fa9aca4d8c52ebff4ab4f390196ab0f1/Night_at_the_Museum_Secret_of_the_Tomb_Wow_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/47qSLctyurmw22A3z7X1YG/8651f02d7e58ebb1eb312a26f732515b/Night_at_the_Museum_Secret_of_the_Tomb_Wow_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/3ZLiolsGZkiTp3p8aLJb3d/e330311fcaed4716f7a603d058ee8767/Night_at_the_Museum_Secret_of_the_Tomb_Wow_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1ZwmDYe0zdKzsy1MJrANSE/e5e0f5ea1c745baf61b205b6e6574605/Night_at_the_Museum_Secret_of_the_Tomb_Wow_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/5o3CSC67KEoIojYH0UMghP/fbf9dea48ac1914eb7f7bffca802b012/Night_at_the_Museum_Secret_of_the_Tomb_Wow.mp3\"\n  },\n  {\n    \"movie\": \"The Internship\",\n    \"year\": 2013,\n    \"release_date\": \"2013-05-29\",\n    \"director\": \"Shawn Levy\",\n    \"character\": \"Nick Campbell\",\n    \"movie_duration\": \"02:05:02\",\n    \"timestamp\": \"01:26:54\",\n    \"full_line\": \"You're so cute. It's beautiful. Wow.\",\n    \"current_wow_in_movie\": 4,\n    \"total_wows_in_movie\": 5,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/6XhmTpc1PoiTf0q1nDYMyv/594947f72b4dba7f3c24938a680dd603/The_Internship_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/61gtrYzY8qsD3Onc4hUNdx/e1685933255e13fce8578594dbfc30aa/The_Internship_Wow_4_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/3CQQDswj7JP1UU95KdHZ2x/af3e2709d1b8c730a177a56e0395bce3/The_Internship_Wow_4_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/67gEv3fXOzKu5IsscIYAxw/bf8e18b6543eec4ac1952d6c6845cb8f/The_Internship_Wow_4_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1ZrN3pCfLhimJ2fxxf86n3/43116038f56d789a1514778081c7dcc2/The_Internship_Wow_4_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/5Jphk5c8qFsc7ZiWNeJOKQ/80f43d3758ee8730987c6a859ffec667/The_Internship_Wow_4.mp3\"\n  },\n  {\n    \"movie\": \"Cars 3\",\n    \"year\": 2017,\n    \"release_date\": \"2017-05-23\",\n    \"director\": \"Brian Fee\",\n    \"character\": \"Lighting McQueen\",\n    \"movie_duration\": \"01:42:25\",\n    \"timestamp\": \"00:22:42\",\n    \"full_line\": \"Wow!\",\n    \"current_wow_in_movie\": 2,\n    \"total_wows_in_movie\": 10,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/43fOBsgY8iOJL0dijvFnfl/ea361efc5131a859c173ab5dd3fdfe1e/Cars_3_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1xpGU65BqXMplnWjcs0HG7/19ad3a66f22c976b7a345ba7a2d931ce/Cars_3_Wow_2_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/NXF2Haueh2DGbaHOpLDuV/21d43d80d211f6f93062b4f35c6d68ba/Cars_3_Wow_2_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6mN4TgA3TUcgqUEec3qCOV/c30c18bc851f43db95b3cab17aea0fb0/Cars_3_Wow_2_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/316quJhdUfAkcmKkzPjww0/1780af73d03a7a631c71429d59a064e9/Cars_3_Wow_2_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/7rkrQVW7vsUj6amBflfTQ9/fe38afabd1eeaed70d14a8bbcf68d417/Cars_3_Wow_2.mp3\"\n  },\n  {\n    \"movie\": \"Hall Pass\",\n    \"year\": 2011,\n    \"release_date\": \"2011-02-25\",\n    \"director\": \"Peter Farrelly and Bobby Farrelly\",\n    \"character\": \"Rick Mills\",\n    \"movie_duration\": \"01:51:35\",\n    \"timestamp\": \"01:26:28\",\n    \"full_line\": \"Wow.\",\n    \"current_wow_in_movie\": 6,\n    \"total_wows_in_movie\": 6,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/6jFEUPmYiKifaTuC2cugm8/22087834d091445fc9393cdd9163a901/Hall_Pass_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/V9JGQ3qgfufm4vUkzyJYI/6182dce642b9f31ab698d7ab448688bd/Hall_Pass_Wow_6_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/3RkKyI4NFfakkYzUEO83hG/21d29c6b7695d1b072084a92e05b6283/Hall_Pass_Wow_6_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/54Zd2b5BQrQi4hvZ8IAg8j/9a719307cdf6525257e0dba0afa9ceee/Hall_Pass_Wow_6_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7Jo3JA6zl3vzAiIkriLLOR/5f488e12cdc1c7792484f098732b790d/Hall_Pass_Wow_6_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/7mZxDvl1IsR95NAg8mKj0t/43a0b44fd325c1de3f86cf6cec3a80d5/Hall_Pass_Wow_6.mp3\"\n  },\n  {\n    \"movie\": \"Cars\",\n    \"year\": 2006,\n    \"release_date\": \"2006-06-09\",\n    \"director\": \"John Lasseter\",\n    \"character\": \"Lightning McQueen\",\n    \"movie_duration\": \"01:56:36\",\n    \"timestamp\": \"01:23:13\",\n    \"full_line\": \"Wow, you were right!\",\n    \"current_wow_in_movie\": 4,\n    \"total_wows_in_movie\": 5,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/6dsHUil72TJLYqbwYMEjH4/387a2d9994a2f1fb069d970a0f30ba32/Cars_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/5WH68GXLSVdosuz0ouwFDF/0b55b456669a8bf8a13e6aecd46cae89/Cars_Wow_4_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7KSwlI2izHJDNtiGAH32SU/7d7698ac27be72753ddc1e2291399b46/Cars_Wow_4_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6OExsgI6VJHXcjNZtlqBkx/3152bd6700486416ec0b818829de9374/Cars_Wow_4_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/2ibZvAYOWwmoqQtANL3d8o/9c09f29a7f42ecd996ac991b52b2cc65/Cars_Wow_4_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/iJ5LIuyc3pmvEAaiSVSLS/85e36dc0324b28549ea56a6882a03062/Cars_Wow_4.mp3\"\n  },\n  {\n    \"movie\": \"The Big Bounce\",\n    \"year\": 2004,\n    \"release_date\": \"2004-01-30\",\n    \"director\": \"George Armitage\",\n    \"character\": \"Jack Ryan\",\n    \"movie_duration\": \"01:28:10\",\n    \"timestamp\": \"00:58:00\",\n    \"full_line\": \"What is it about the produce section? Wow.\",\n    \"current_wow_in_movie\": 2,\n    \"total_wows_in_movie\": 2,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/4hPpMWXhuXQ8HzLusmcaS5/4c46fa4fa8bce13b06d8b4444f6150ef/The_Big_Bounce_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6TPALdqhVTMztHMSw2qOOj/81d512b2ee8b7c05e7e851eae7790d0e/The_Big_Bounce_Wow_2_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/dEuiSxnBLWP26YHaWiHbz/599ac6fb7a6aead87e8e6efdab759f74/The_Big_Bounce_Wow_2_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7tA15W0lgJr7CpJNCf1m4Z/86a953df81d88f5fa79e35aa1e4e4dc1/The_Big_Bounce_Wow_2_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1iKZTtL0qQRDek4y4SduD5/bbd83798b30435b43a2df4c360130d72/The_Big_Bounce_Wow_2_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/6pjOYj0xK38VAkPAl1QK7R/a5f083c60ce9e9c4fa93a8be70817d22/The_Big_Bounce_Wow_2.mp3\"\n  },\n  {\n    \"movie\": \"The Big Year\",\n    \"year\": 2011,\n    \"release_date\": \"2011-10-14\",\n    \"director\": \"David Frankel\",\n    \"character\": \"Kenny Bostick\",\n    \"movie_duration\": \"01:43:09\",\n    \"timestamp\": \"01:08:23\",\n    \"full_line\": \"Wow, and here I thought I was the bomb at 728.\",\n    \"current_wow_in_movie\": 3,\n    \"total_wows_in_movie\": 3,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/pCjGOhbTCQVjLRN9zTwIi/ce7cdf4b40f3549326d881697aa468a1/The_Big_Year_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/uOdKu6VANHZjtwZXeBU8M/bf1f0f44c7bc01faa648a1b97b1205d3/The_Big_Year_Wow_3_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/y9wR96fHZaNnZr367mjyI/1262c2af0400cb1dc8528c3c8e6a83a4/The_Big_Year_Wow_3_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/55z94kbUxNFIL0rYb1i2mH/ca0505ca5e2017b663f18cca8af571b2/The_Big_Year_Wow_3_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6moRL0SzmkTGzgEuZeVByI/c63fa33d0992c0594ecbaed4fcd7e891/The_Big_Year_Wow_3_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/1dXEYZQP1Fl6CX1roy7ZAH/f31b90a6f961dcc16c16b294f4faf664/The_Big_Year_Wow_3.mp3\"\n  },\n  {\n    \"movie\": \"Hall Pass\",\n    \"year\": 2011,\n    \"release_date\": \"2011-02-25\",\n    \"director\": \"Peter Farrelly and Bobby Farrelly\",\n    \"character\": \"Rick Mills\",\n    \"movie_duration\": \"01:51:35\",\n    \"timestamp\": \"00:46:55\",\n    \"full_line\": \"Wow. This is awkward. I feel like I'm at my first junior high mixer. You know? When you don't know what to say.\",\n    \"current_wow_in_movie\": 3,\n    \"total_wows_in_movie\": 6,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/6jFEUPmYiKifaTuC2cugm8/22087834d091445fc9393cdd9163a901/Hall_Pass_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/4QcL02MHJ8ApVkbfN8cP6E/264c28c1e9195d87f0206e143c5ca54a/Hall_Pass_Wow_3_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/15h0sMoIhdeaPDB8qSsUN9/36245f66352b595dc40bc4d9903fa5b3/Hall_Pass_Wow_3_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/74fQiVcwuT7ePQemGC7ih4/b102922c97c9ff38f47268d648628a22/Hall_Pass_Wow_3_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7mSGl1rSVtGdfSacwnKVsu/e7ac36e5684f6b64978987d2f68c43db/Hall_Pass_Wow_3_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/2NBIVPDF4o7cy0epTvPOwR/406cd5c17e9b01511f1e350bb96df352/Hall_Pass_Wow_3.mp3\"\n  },\n  {\n    \"movie\": \"No Escape\",\n    \"year\": 2015,\n    \"release_date\": \"2015-08-26\",\n    \"director\": \"John Erick Dowdle\",\n    \"character\": \"Jack Dwyer\",\n    \"movie_duration\": \"01:43:06\",\n    \"timestamp\": \"00:08:11\",\n    \"full_line\": \"Wow.\",\n    \"current_wow_in_movie\": 1,\n    \"total_wows_in_movie\": 2,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/R3PGhNHo0k5uhnPM7bqIp/5e2b3723c69f7c70313be19417c6fdba/No_Escape_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/3WxIt1hZQtpqnveyAb4xw3/54773cfd70ab4380c47048324f216fc4/No_Escape_Wow_1_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/2JcYMonzmocD3qABm1dpNg/019b752a33b2517033d75ea5bfd20b59/No_Escape_Wow_1_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/5QMtRpqqyfg9irqjPPx5JO/5a24212c22dc20944b5831e5b81efe4d/No_Escape_Wow_1_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6aou6AH16gfvXSccxgcZFf/347d5ff951c851d5fe9928323e6067eb/No_Escape_Wow_1_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/24lDtvWBG8mDqaqBM8Qsr0/4c08f87367dd3399efb6a5554f6f4858/No_Escape_Wow_1.mp3\"\n  },\n  {\n    \"movie\": \"Midnight in Paris\",\n    \"year\": 2011,\n    \"release_date\": \"2011-05-20\",\n    \"director\": \"Woody Allen\",\n    \"character\": \"Gil Pender\",\n    \"movie_duration\": \"01:33:57\",\n    \"timestamp\": \"00:58:22\",\n    \"full_line\": \"Wow.\",\n    \"current_wow_in_movie\": 5,\n    \"total_wows_in_movie\": 10,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/2ZcfSCe2dlfoVzYMr4b9nK/d566e5ad044dee56645f3bffc7200d64/Midnight_in_Paris_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1DSaYjQ8SnL1Imeuxe0eXE/100d3106c8a0bf5e80372e2187daf325/Midnight_in_Paris_Wow_5_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/eFI1n7voe4CsxkWEWv2q0/36781ed8f50e508e8e1f79c8e65e601a/Midnight_in_Paris_Wow_5_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6oSaIzfy7k3DJMvvy3j1kN/d77192d7decb3ba9d06a5210a58df1fa/Midnight_in_Paris_Wow_5_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/4LQ4lkhx41XDC6UcMlmjbA/dfd5e3604c85c9fb9d38ea2dfdd02a2f/Midnight_in_Paris_Wow_5_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/2A5G34x8JCQBuYwU4D2l9S/27cc467b3ff5796ff1bbe113a06a6e64/Midnight_in_Paris_Wow_5.mp3\"\n  },\n  {\n    \"movie\": \"The Haunting\",\n    \"year\": 1999,\n    \"release_date\": \"1999-07-23\",\n    \"director\": \"Jan de Bont\",\n    \"character\": \"Luke Sanderson\",\n    \"movie_duration\": \"01:52:37\",\n    \"timestamp\": \"00:41:34\",\n    \"full_line\": \"Wow, I sorta got screwed on the ol' bedroom selection.\",\n    \"current_wow_in_movie\": 4,\n    \"total_wows_in_movie\": 5,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/6Zu7ux0JYrUWk0UC8vxPtj/d0da8bfbad670655779a9921ac514759/The_Haunting_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/01C128tlGJ79vtlFEPc7V9/184759a9ca6fb1ab57114977afccb2ff/The_Haunting_Wow_4_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/3fR8AigFIp2I1vkSLRhzur/afb0c226daf7806bc9bd9c5e7b008fb4/The_Haunting_Wow_4_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1OCimrvhXGGfffHqBKaqz7/52d16b12a2d1a8a264b09364afc4c87c/The_Haunting_Wow_4_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6bwt57tdhBCbhsXZ4i0pce/7f6debc7e52f80c78a618f19c8087985/The_Haunting_Wow_4_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/3yvd3DvmY0XUyqWPLUYLqp/1aabc0f7a6b58003ccdaa61469b8d5ee/The_Haunting_Wow_4.mp3\"\n  },\n  {\n    \"movie\": \"Cars 3\",\n    \"year\": 2017,\n    \"release_date\": \"2017-05-23\",\n    \"director\": \"Brian Fee\",\n    \"character\": \"Lighting McQueen\",\n    \"movie_duration\": \"01:42:25\",\n    \"timestamp\": \"00:26:23\",\n    \"full_line\": \"Wow.\",\n    \"current_wow_in_movie\": 5,\n    \"total_wows_in_movie\": 10,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/43fOBsgY8iOJL0dijvFnfl/ea361efc5131a859c173ab5dd3fdfe1e/Cars_3_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/4Vp8pVwekVrXfmHT3xYCBm/5775501444824d3ee90acda0698e3472/Cars_3_Wow_5_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6IoA0jwJiPwyXcSbDCrbJ5/5e65c8caa57c62ce9fc862aaeb327255/Cars_3_Wow_5_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/JrOrzMzr2aHCVPkRJUCae/06c5cb7543e95211a6f99ed89e6ea498/Cars_3_Wow_5_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6uufhJa3yX9e0Ib0xsOb46/16f9e55246ad32a33a34d58e621b8942/Cars_3_Wow_5_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/4aUtZzrqLHol2Hhb52zgHQ/ed5c4ef03ebba9ae34659e9058e58461/Cars_3_Wow_5.mp3\"\n  },\n  {\n    \"movie\": \"Cars 3\",\n    \"year\": 2017,\n    \"release_date\": \"2017-05-23\",\n    \"director\": \"Brian Fee\",\n    \"character\": \"Lighting McQueen\",\n    \"movie_duration\": \"01:42:25\",\n    \"timestamp\": \"00:26:11\",\n    \"full_line\": \"Wow.\",\n    \"current_wow_in_movie\": 4,\n    \"total_wows_in_movie\": 10,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/43fOBsgY8iOJL0dijvFnfl/ea361efc5131a859c173ab5dd3fdfe1e/Cars_3_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7G6jLS5xfYBOepUxkW7R3c/c78d22f63ba51f371a1550834888b9ca/Cars_3_Wow_4_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/lIhPcmNS5L4DnpY41EpUA/96b0c5b44acf3fe16e68d1e67156d41c/Cars_3_Wow_4_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1h3am3BfJYUm0VXV4uKF9P/e75775ec0621889efd40ce1bc372735a/Cars_3_Wow_4_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6eO8c3ojQA4rWMlE1hMMOa/c3a9ce4c01b2d342b33144c9f90b9e3e/Cars_3_Wow_4_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/6aJuGg3bTjqd20j59vP1Wv/6cb60311194d3a9aa040882911a5f516/Cars_3_Wow_4.mp3\"\n  },\n  {\n    \"movie\": \"Midnight in Paris\",\n    \"year\": 2011,\n    \"release_date\": \"2011-05-20\",\n    \"director\": \"Woody Allen\",\n    \"character\": \"Gil Pender\",\n    \"movie_duration\": \"01:33:57\",\n    \"timestamp\": \"01:19:23\",\n    \"full_line\": \"Wow. Yes. Yes. Yes.\",\n    \"current_wow_in_movie\": 7,\n    \"total_wows_in_movie\": 10,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/2ZcfSCe2dlfoVzYMr4b9nK/d566e5ad044dee56645f3bffc7200d64/Midnight_in_Paris_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7N21xMDnGKfrmBO42xvoc9/c8e05e53e5841c6ba8a659aaf4f6736a/Midnight_in_Paris_Wow_7_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6CF2q8U9OYgt3yFA7JUBkr/d6ddbf8d57b1cf9d38127735a6266431/Midnight_in_Paris_Wow_7_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/4IoMNf4pF7L3wDZ72Ad31d/efd2e6859fecf3a82a7ecdaf43175826/Midnight_in_Paris_Wow_7_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/5yF8k9rwFj99H69WASEZWU/f9741ed0771da2afd9a26454863137bb/Midnight_in_Paris_Wow_7_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/UmpPa6skALBubhqeKQFpn/2a64c51d16877b8c0bf8c23a74749f7e/Midnight_in_Paris_Wow_7.mp3\"\n  },\n  {\n    \"movie\": \"Midnight in Paris\",\n    \"year\": 2011,\n    \"release_date\": \"2011-05-20\",\n    \"director\": \"Woody Allen\",\n    \"character\": \"Gil Pender\",\n    \"movie_duration\": \"01:33:57\",\n    \"timestamp\": \"00:48:45\",\n    \"full_line\": \"Wow.\",\n    \"current_wow_in_movie\": 3,\n    \"total_wows_in_movie\": 10,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/2ZcfSCe2dlfoVzYMr4b9nK/d566e5ad044dee56645f3bffc7200d64/Midnight_in_Paris_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/7BBJiej0qdBbuB3Xvimrae/932bc77c9e9a9918949709f846035541/Midnight_in_Paris_Wow_3_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/5tBiVUM6ef2mARH3OXzP92/8ad999e82b7bff5ac4a07d1b4dcb45f3/Midnight_in_Paris_Wow_3_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/39zl8hKK4QP1kASldccYyN/7fc8a7c86872abb9fd527a350365beea/Midnight_in_Paris_Wow_3_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6rl512MqIH6r04L4O9jHU2/2d843c19937d770860f17719960d8352/Midnight_in_Paris_Wow_3_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/7BDkqWADSOxxYOSQGqNRcr/6fc4696de0d8bcafa487ddea97bb6d3b/Midnight_in_Paris_Wow_3.mp3\"\n  },\n  {\n    \"movie\": \"The Internship\",\n    \"year\": 2013,\n    \"release_date\": \"2013-05-29\",\n    \"director\": \"Shawn Levy\",\n    \"character\": \"Nick Campbell\",\n    \"movie_duration\": \"02:05:02\",\n    \"timestamp\": \"01:27:38\",\n    \"full_line\": \"Wow, a little heart and everything.\",\n    \"current_wow_in_movie\": 5,\n    \"total_wows_in_movie\": 5,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/6XhmTpc1PoiTf0q1nDYMyv/594947f72b4dba7f3c24938a680dd603/The_Internship_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1tZ97B7BWV7WhuYJSRL0aQ/c86ca6ce4c67a0f1f5343e68a7b1f428/The_Internship_Wow_5_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/2QlKc3s0Nf10vMLaKGFhVf/a76e1c99d80b13687ae593ad95837706/The_Internship_Wow_5_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/2t0rkVxOB7MVJVzUtVobAV/c396fd523e190052d4b009081b22423f/The_Internship_Wow_5_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/1W9V229ebo4J74HLZGO7pF/f4379d501080fc77221ca8df020a7cd6/The_Internship_Wow_5_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/30nYCeovpGBN5dwVmnTxrf/62c07aaab901bb488bc53ed61d2edffc/The_Internship_Wow_5.mp3\"\n  },\n  {\n    \"movie\": \"The Big Year\",\n    \"year\": 2011,\n    \"release_date\": \"2011-10-14\",\n    \"director\": \"David Frankel\",\n    \"character\": \"Kenny Bostick\",\n    \"movie_duration\": \"01:43:09\",\n    \"timestamp\": \"00:44:48\",\n    \"full_line\": \"Wow!\",\n    \"current_wow_in_movie\": 1,\n    \"total_wows_in_movie\": 3,\n    \"poster\": \"https://images.ctfassets.net/bs8ntwkklfua/pCjGOhbTCQVjLRN9zTwIi/ce7cdf4b40f3549326d881697aa468a1/The_Big_Year_Poster.jpg\",\n    \"video\": {\n      \"1080p\": \"https://videos.ctfassets.net/bs8ntwkklfua/2IZMW5Aeytz5rtu5MzcFk8/f675c3f6ef153c5df0c81679c80677d4/The_Big_Year_Wow_1_1080p.mp4\",\n      \"720p\": \"https://videos.ctfassets.net/bs8ntwkklfua/2K0XjRiHYd8zbdYcmiTMH4/b2e4a9b02db184a4d4bd71fe15ab37a9/The_Big_Year_Wow_1_720p.mp4\",\n      \"480p\": \"https://videos.ctfassets.net/bs8ntwkklfua/6QMkzBk6HrfuVxVT97mQg/5877904b163d7ccdc4baa92d449858d8/The_Big_Year_Wow_1_480p.mp4\",\n      \"360p\": \"https://videos.ctfassets.net/bs8ntwkklfua/5SFeCoTmyTUEJYtK0kOO92/462afffbd6315182db59271f99a5a829/The_Big_Year_Wow_1_360p.mp4\"\n    },\n    \"audio\": \"https://assets.ctfassets.net/bs8ntwkklfua/45tt9xH4Uf3Rgt0EJtykZZ/b5cef0c5d09d368359f4a9c0e66a6663/The_Big_Year_Wow_1.mp3\"\n  }\n]\n"
  },
  {
    "path": "examples/pokemon.json",
    "content": "{\n  \"abilities\": [\n    {\n      \"ability\": {\n        \"name\": \"limber\",\n        \"url\": \"https://pokeapi.co/api/v2/ability/7/\"\n      },\n      \"is_hidden\": false,\n      \"slot\": 1\n    },\n    {\n      \"ability\": {\n        \"name\": \"imposter\",\n        \"url\": \"https://pokeapi.co/api/v2/ability/150/\"\n      },\n      \"is_hidden\": true,\n      \"slot\": 3\n    }\n  ],\n  \"base_experience\": 101,\n  \"forms\": [\n    {\n      \"name\": \"ditto\",\n      \"url\": \"https://pokeapi.co/api/v2/pokemon-form/132/\"\n    }\n  ],\n  \"game_indices\": [\n    {\n      \"game_index\": 76,\n      \"version\": {\n        \"name\": \"red\",\n        \"url\": \"https://pokeapi.co/api/v2/version/1/\"\n      }\n    },\n    {\n      \"game_index\": 76,\n      \"version\": {\n        \"name\": \"blue\",\n        \"url\": \"https://pokeapi.co/api/v2/version/2/\"\n      }\n    },\n    {\n      \"game_index\": 76,\n      \"version\": {\n        \"name\": \"yellow\",\n        \"url\": \"https://pokeapi.co/api/v2/version/3/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"gold\",\n        \"url\": \"https://pokeapi.co/api/v2/version/4/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"silver\",\n        \"url\": \"https://pokeapi.co/api/v2/version/5/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"crystal\",\n        \"url\": \"https://pokeapi.co/api/v2/version/6/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"ruby\",\n        \"url\": \"https://pokeapi.co/api/v2/version/7/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"sapphire\",\n        \"url\": \"https://pokeapi.co/api/v2/version/8/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"emerald\",\n        \"url\": \"https://pokeapi.co/api/v2/version/9/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"firered\",\n        \"url\": \"https://pokeapi.co/api/v2/version/10/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"leafgreen\",\n        \"url\": \"https://pokeapi.co/api/v2/version/11/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"diamond\",\n        \"url\": \"https://pokeapi.co/api/v2/version/12/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"pearl\",\n        \"url\": \"https://pokeapi.co/api/v2/version/13/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"platinum\",\n        \"url\": \"https://pokeapi.co/api/v2/version/14/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"heartgold\",\n        \"url\": \"https://pokeapi.co/api/v2/version/15/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"soulsilver\",\n        \"url\": \"https://pokeapi.co/api/v2/version/16/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"black\",\n        \"url\": \"https://pokeapi.co/api/v2/version/17/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"white\",\n        \"url\": \"https://pokeapi.co/api/v2/version/18/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"black-2\",\n        \"url\": \"https://pokeapi.co/api/v2/version/21/\"\n      }\n    },\n    {\n      \"game_index\": 132,\n      \"version\": {\n        \"name\": \"white-2\",\n        \"url\": \"https://pokeapi.co/api/v2/version/22/\"\n      }\n    }\n  ],\n  \"height\": 3,\n  \"held_items\": [\n    {\n      \"item\": {\n        \"name\": \"metal-powder\",\n        \"url\": \"https://pokeapi.co/api/v2/item/234/\"\n      },\n      \"version_details\": [\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"ruby\",\n            \"url\": \"https://pokeapi.co/api/v2/version/7/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"sapphire\",\n            \"url\": \"https://pokeapi.co/api/v2/version/8/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"emerald\",\n            \"url\": \"https://pokeapi.co/api/v2/version/9/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"firered\",\n            \"url\": \"https://pokeapi.co/api/v2/version/10/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"leafgreen\",\n            \"url\": \"https://pokeapi.co/api/v2/version/11/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"diamond\",\n            \"url\": \"https://pokeapi.co/api/v2/version/12/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"pearl\",\n            \"url\": \"https://pokeapi.co/api/v2/version/13/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"platinum\",\n            \"url\": \"https://pokeapi.co/api/v2/version/14/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"heartgold\",\n            \"url\": \"https://pokeapi.co/api/v2/version/15/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"soulsilver\",\n            \"url\": \"https://pokeapi.co/api/v2/version/16/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"black\",\n            \"url\": \"https://pokeapi.co/api/v2/version/17/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"white\",\n            \"url\": \"https://pokeapi.co/api/v2/version/18/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"black-2\",\n            \"url\": \"https://pokeapi.co/api/v2/version/21/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"white-2\",\n            \"url\": \"https://pokeapi.co/api/v2/version/22/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"x\",\n            \"url\": \"https://pokeapi.co/api/v2/version/23/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"y\",\n            \"url\": \"https://pokeapi.co/api/v2/version/24/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"omega-ruby\",\n            \"url\": \"https://pokeapi.co/api/v2/version/25/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"alpha-sapphire\",\n            \"url\": \"https://pokeapi.co/api/v2/version/26/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"sun\",\n            \"url\": \"https://pokeapi.co/api/v2/version/27/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"moon\",\n            \"url\": \"https://pokeapi.co/api/v2/version/28/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"ultra-sun\",\n            \"url\": \"https://pokeapi.co/api/v2/version/29/\"\n          }\n        },\n        {\n          \"rarity\": 5,\n          \"version\": {\n            \"name\": \"ultra-moon\",\n            \"url\": \"https://pokeapi.co/api/v2/version/30/\"\n          }\n        }\n      ]\n    },\n    {\n      \"item\": {\n        \"name\": \"quick-powder\",\n        \"url\": \"https://pokeapi.co/api/v2/item/251/\"\n      },\n      \"version_details\": [\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"diamond\",\n            \"url\": \"https://pokeapi.co/api/v2/version/12/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"pearl\",\n            \"url\": \"https://pokeapi.co/api/v2/version/13/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"platinum\",\n            \"url\": \"https://pokeapi.co/api/v2/version/14/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"heartgold\",\n            \"url\": \"https://pokeapi.co/api/v2/version/15/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"soulsilver\",\n            \"url\": \"https://pokeapi.co/api/v2/version/16/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"black\",\n            \"url\": \"https://pokeapi.co/api/v2/version/17/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"white\",\n            \"url\": \"https://pokeapi.co/api/v2/version/18/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"black-2\",\n            \"url\": \"https://pokeapi.co/api/v2/version/21/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"white-2\",\n            \"url\": \"https://pokeapi.co/api/v2/version/22/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"x\",\n            \"url\": \"https://pokeapi.co/api/v2/version/23/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"y\",\n            \"url\": \"https://pokeapi.co/api/v2/version/24/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"omega-ruby\",\n            \"url\": \"https://pokeapi.co/api/v2/version/25/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"alpha-sapphire\",\n            \"url\": \"https://pokeapi.co/api/v2/version/26/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"sun\",\n            \"url\": \"https://pokeapi.co/api/v2/version/27/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"moon\",\n            \"url\": \"https://pokeapi.co/api/v2/version/28/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"ultra-sun\",\n            \"url\": \"https://pokeapi.co/api/v2/version/29/\"\n          }\n        },\n        {\n          \"rarity\": 50,\n          \"version\": {\n            \"name\": \"ultra-moon\",\n            \"url\": \"https://pokeapi.co/api/v2/version/30/\"\n          }\n        }\n      ]\n    }\n  ],\n  \"id\": 132,\n  \"is_default\": true,\n  \"location_area_encounters\": \"https://pokeapi.co/api/v2/pokemon/132/encounters\",\n  \"moves\": [\n    {\n      \"move\": {\n        \"name\": \"transform\",\n        \"url\": \"https://pokeapi.co/api/v2/move/144/\"\n      },\n      \"version_group_details\": [\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"red-blue\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/1/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"yellow\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/2/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"gold-silver\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/3/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"crystal\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/4/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"ruby-sapphire\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/5/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"emerald\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/6/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"firered-leafgreen\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/7/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"diamond-pearl\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/8/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"platinum\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/9/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"heartgold-soulsilver\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/10/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"black-white\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/11/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"colosseum\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/12/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"xd\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/13/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"black-2-white-2\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/14/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"x-y\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/15/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"omega-ruby-alpha-sapphire\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/16/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"sun-moon\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/17/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"ultra-sun-ultra-moon\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/18/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"lets-go-pikachu-lets-go-eevee\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/19/\"\n          }\n        },\n        {\n          \"level_learned_at\": 1,\n          \"move_learn_method\": {\n            \"name\": \"level-up\",\n            \"url\": \"https://pokeapi.co/api/v2/move-learn-method/1/\"\n          },\n          \"version_group\": {\n            \"name\": \"sword-shield\",\n            \"url\": \"https://pokeapi.co/api/v2/version-group/20/\"\n          }\n        }\n      ]\n    }\n  ],\n  \"name\": \"ditto\",\n  \"order\": 214,\n  \"past_types\": [],\n  \"species\": {\n    \"name\": \"ditto\",\n    \"url\": \"https://pokeapi.co/api/v2/pokemon-species/132/\"\n  },\n  \"sprites\": {\n    \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/132.png\",\n    \"back_female\": null,\n    \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/132.png\",\n    \"back_shiny_female\": null,\n    \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/132.png\",\n    \"front_female\": null,\n    \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/132.png\",\n    \"front_shiny_female\": null,\n    \"other\": {\n      \"dream_world\": {\n        \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/132.svg\",\n        \"front_female\": null\n      },\n      \"home\": {\n        \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/132.png\",\n        \"front_female\": null,\n        \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/shiny/132.png\",\n        \"front_shiny_female\": null\n      },\n      \"official-artwork\": {\n        \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/132.png\"\n      }\n    },\n    \"versions\": {\n      \"generation-i\": {\n        \"red-blue\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/132.png\",\n          \"back_gray\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/gray/132.png\",\n          \"back_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/transparent/back/132.png\",\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/132.png\",\n          \"front_gray\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/gray/132.png\",\n          \"front_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/transparent/132.png\"\n        },\n        \"yellow\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/back/132.png\",\n          \"back_gray\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/back/gray/132.png\",\n          \"back_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/transparent/back/132.png\",\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/132.png\",\n          \"front_gray\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/gray/132.png\",\n          \"front_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/transparent/132.png\"\n        }\n      },\n      \"generation-ii\": {\n        \"crystal\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/back/132.png\",\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/back/shiny/132.png\",\n          \"back_shiny_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/back/shiny/132.png\",\n          \"back_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/back/132.png\",\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/132.png\",\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/shiny/132.png\",\n          \"front_shiny_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/shiny/132.png\",\n          \"front_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/132.png\"\n        },\n        \"gold\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/back/132.png\",\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/back/shiny/132.png\",\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/132.png\",\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/shiny/132.png\",\n          \"front_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/transparent/132.png\"\n        },\n        \"silver\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/back/132.png\",\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/back/shiny/132.png\",\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/132.png\",\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/shiny/132.png\",\n          \"front_transparent\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/transparent/132.png\"\n        }\n      },\n      \"generation-iii\": {\n        \"emerald\": {\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/emerald/132.png\",\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/emerald/shiny/132.png\"\n        },\n        \"firered-leafgreen\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/back/132.png\",\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/back/shiny/132.png\",\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/132.png\",\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/shiny/132.png\"\n        },\n        \"ruby-sapphire\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/back/132.png\",\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/back/shiny/132.png\",\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/132.png\",\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/shiny/132.png\"\n        }\n      },\n      \"generation-iv\": {\n        \"diamond-pearl\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/back/132.png\",\n          \"back_female\": null,\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/back/shiny/132.png\",\n          \"back_shiny_female\": null,\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/132.png\",\n          \"front_female\": null,\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/shiny/132.png\",\n          \"front_shiny_female\": null\n        },\n        \"heartgold-soulsilver\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/back/132.png\",\n          \"back_female\": null,\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/back/shiny/132.png\",\n          \"back_shiny_female\": null,\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/132.png\",\n          \"front_female\": null,\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/shiny/132.png\",\n          \"front_shiny_female\": null\n        },\n        \"platinum\": {\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/back/132.png\",\n          \"back_female\": null,\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/back/shiny/132.png\",\n          \"back_shiny_female\": null,\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/132.png\",\n          \"front_female\": null,\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/shiny/132.png\",\n          \"front_shiny_female\": null\n        }\n      },\n      \"generation-v\": {\n        \"black-white\": {\n          \"animated\": {\n            \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/back/132.gif\",\n            \"back_female\": null,\n            \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/back/shiny/132.gif\",\n            \"back_shiny_female\": null,\n            \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/132.gif\",\n            \"front_female\": null,\n            \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/shiny/132.gif\",\n            \"front_shiny_female\": null\n          },\n          \"back_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/back/132.png\",\n          \"back_female\": null,\n          \"back_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/back/shiny/132.png\",\n          \"back_shiny_female\": null,\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/132.png\",\n          \"front_female\": null,\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/shiny/132.png\",\n          \"front_shiny_female\": null\n        }\n      },\n      \"generation-vi\": {\n        \"omegaruby-alphasapphire\": {\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/omegaruby-alphasapphire/132.png\",\n          \"front_female\": null,\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/omegaruby-alphasapphire/shiny/132.png\",\n          \"front_shiny_female\": null\n        },\n        \"x-y\": {\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/x-y/132.png\",\n          \"front_female\": null,\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/x-y/shiny/132.png\",\n          \"front_shiny_female\": null\n        }\n      },\n      \"generation-vii\": {\n        \"icons\": {\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/icons/132.png\",\n          \"front_female\": null\n        },\n        \"ultra-sun-ultra-moon\": {\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/ultra-sun-ultra-moon/132.png\",\n          \"front_female\": null,\n          \"front_shiny\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/ultra-sun-ultra-moon/shiny/132.png\",\n          \"front_shiny_female\": null\n        }\n      },\n      \"generation-viii\": {\n        \"icons\": {\n          \"front_default\": \"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-viii/icons/132.png\",\n          \"front_female\": null\n        }\n      }\n    }\n  },\n  \"stats\": [\n    {\n      \"base_stat\": 48,\n      \"effort\": 1,\n      \"stat\": {\n        \"name\": \"hp\",\n        \"url\": \"https://pokeapi.co/api/v2/stat/1/\"\n      }\n    },\n    {\n      \"base_stat\": 48,\n      \"effort\": 0,\n      \"stat\": {\n        \"name\": \"attack\",\n        \"url\": \"https://pokeapi.co/api/v2/stat/2/\"\n      }\n    },\n    {\n      \"base_stat\": 48,\n      \"effort\": 0,\n      \"stat\": {\n        \"name\": \"defense\",\n        \"url\": \"https://pokeapi.co/api/v2/stat/3/\"\n      }\n    },\n    {\n      \"base_stat\": 48,\n      \"effort\": 0,\n      \"stat\": {\n        \"name\": \"special-attack\",\n        \"url\": \"https://pokeapi.co/api/v2/stat/4/\"\n      }\n    },\n    {\n      \"base_stat\": 48,\n      \"effort\": 0,\n      \"stat\": {\n        \"name\": \"special-defense\",\n        \"url\": \"https://pokeapi.co/api/v2/stat/5/\"\n      }\n    },\n    {\n      \"base_stat\": 48,\n      \"effort\": 0,\n      \"stat\": {\n        \"name\": \"speed\",\n        \"url\": \"https://pokeapi.co/api/v2/stat/6/\"\n      }\n    }\n  ],\n  \"types\": [\n    {\n      \"slot\": 1,\n      \"type\": {\n        \"name\": \"normal\",\n        \"url\": \"https://pokeapi.co/api/v2/type/1/\"\n      }\n    }\n  ],\n  \"weight\": 40\n}\n"
  },
  {
    "path": "examples/ronSwansonQuotes.json",
    "content": "[\n  \"There has never been a sadness that can’t been cured by breakfast food.\",\n  \"Dear frozen yogurt, you are the celery of desserts. Be ice cream or be nothing. Zero stars.\",\n  \"Shorts over six inches are capri pants, shorts under six inches are European.\",\n  \"Sting like a bee. Do not float like a butterfly. That’s ridiculous.\",\n  \"Well, I am not usually one for speeches. So, goodbye.\",\n  \"I change my locks every 16 days.\",\n  \"Are you going to tell a man that he can't fart in his own car?\",\n  \"I love nothing.\",\n  \"Fishing relaxes me. It's like yoga, except I still get to kill something.\",\n  \"My son is several weeks old. He is very familiar with the sound of power tools.\",\n  \"Do you have any history of mental illness in your family? I have an uncle who does yoga.\",\n  \"If there were more food and fewer people, this would be a perfect party.\",\n  \"The three most useless jobs in the world in order are: lawyer, congressman, and doctor.\",\n  \"One rage every three months is permitted. Try not to hurt anyone who doesn't deserve it.\",\n  \"I hate everything.\",\n  \"I’ve had the same haircut since 1978 and I’ve driven the same car since 1991. I’ve used the same wooden comb for three decades. I have one bowl. I still get my milk delivered by horse.\",\n  \"Tom put all my records into this rectangle!\",\n  \"It’s pointless for a human to paint scenes of nature when they can go outside and stand in it.\",\n  \"My first ex-wife’s name is Tammy. My second ex-wife’s name is Tammy. My Mom’s name is Tamara…she goes by Tammy.\",\n  \"Live your life how you want, but don’t confuse drama with happiness.\"\n]\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"name\": \"@jsonhero/jsonhero-web\",\n  \"description\": \"A beautiful JSON viewer for the web\",\n  \"license\": \"apache-2.0\",\n  \"scripts\": {\n    \"clean\": \"rimraf dist\",\n    \"build\": \"npm run build:css && npm run build:search && remix build\",\n    \"build:css\": \"tailwindcss -i ./styles/tailwind.css -o ./app/tailwind.css --minify\",\n    \"build:worker\": \"esbuild --define:process.env.NODE_ENV='\\\"production\\\"' --minify --bundle --sourcemap --outdir=dist ./worker\",\n    \"build:worker:analyze\": \"esbuild --define:process.env.NODE_ENV='\\\"production\\\"' --minify --bundle --sourcemap --metafile=meta.json --outdir=dist ./worker\",\n    \"build:visualize\": \"npm run clean && npm run build && npm run build:worker:analyze && esbuild-visualizer --metadata ./meta.json --exclude *.png\",\n    \"build:search\": \"esbuild ./app/entry.worker.ts --outfile=./public/entry.worker.js --bundle --format=esm --define:process.env.NODE_ENV='\\\"production\\\"'\",\n    \"dev:search\": \"esbuild ./app/entry.worker.ts --outfile=./public/entry.worker.js --bundle --format=esm --define:process.env.NODE_ENV='\\\"development\\\"' --watch\",\n    \"dev:worker\": \"esbuild --define:process.env.NODE_ENV='\\\"development\\\"' --bundle --sourcemap --outdir=dist ./worker\",\n    \"start:worker\": \"miniflare --env .env --build-command \\\"npm run dev:worker\\\" --watch\",\n    \"dev\": \"concurrently \\\"npm run dev:css\\\" \\\"npm run dev:search\\\" \\\"remix watch\\\"\",\n    \"dev:css\": \"tailwindcss -i ./styles/tailwind.css -o ./app/tailwind.css --watch\",\n    \"postinstall\": \"remix setup cloudflare-workers\",\n    \"start\": \"concurrently \\\"npm run dev\\\" \\\"npm run start:worker\\\"\",\n    \"deploy\": \"npm run build && wrangler publish\",\n    \"deploy:production\": \"npm run build && wrangler publish --env production\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:coverage\": \"jest --coverage\",\n    \"build:types\": \"tsc\",\n    \"build:types:watch\": \"tsc --watch\",\n    \"lint\": \"npm run build:types\"\n  },\n  \"dependencies\": {\n    \"@apihero/fetch\": \"^0.1.0\",\n    \"@codemirror/lang-json\": \"^0.19.1\",\n    \"@codemirror/rangeset\": \"^0.19.6\",\n    \"@heroicons/react\": \"^1.0.5\",\n    \"@js-temporal/polyfill\": \"^0.3.0\",\n    \"@jsonhero/fuzzy-json-search\": \"^0.2.0\",\n    \"@jsonhero/json-infer-types\": \"^1.2.x\",\n    \"@jsonhero/json-schema-fns\": \"^0.0.1\",\n    \"@jsonhero/path\": \"^1.0.17\",\n    \"@jsonhero/schema-infer\": \"^0.1.x\",\n    \"@radix-ui/react-dialog\": \"^0.1.7\",\n    \"@radix-ui/react-popover\": \"^0.1.5\",\n    \"@radix-ui/react-tabs\": \"^0.1.5\",\n    \"@radix-ui/react-toast\": \"1.0.0\",\n    \"@remix-run/cloudflare-workers\": \"^1.2.3\",\n    \"@remix-run/react\": \"^1.2.3\",\n    \"@remix-run/serve\": \"^1.2.3\",\n    \"@swan-io/boxed\": \"^0.2.1\",\n    \"@uiw/react-codemirror\": \"^4.3.3\",\n    \"@xmldom/xmldom\": \"^0.8.2\",\n    \"clsx\": \"^1.1.1\",\n    \"color\": \"^4.2.1\",\n    \"downshift\": \"^6.1.7\",\n    \"fathom-client\": \"^3.4.1\",\n    \"framer-motion\": \"^6.2.4\",\n    \"json-source-map\": \"^0.6.1\",\n    \"jwt-decode\": \"^3.1.2\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^3.2.0\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-dropzone\": \"^11.4.2\",\n    \"react-hotkeys-hook\": \"^3.4.4\",\n    \"react-use-intercom\": \"^2.0.0\",\n    \"react-virtual\": \"^2.10.4\",\n    \"remix\": \"^1.2.3\",\n    \"tailwindcss-radix\": \"^1.6.0\",\n    \"tiny-invariant\": \"^1.2.0\",\n    \"ts-pattern\": \"^3.3.4\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^2.2.2\",\n    \"@remix-run/dev\": \"^1.2.3\",\n    \"@tailwindcss/forms\": \"^0.4.0\",\n    \"@types/color\": \"^3.0.3\",\n    \"@types/jest\": \"^27.4.0\",\n    \"@types/lodash-es\": \"^4.17.6\",\n    \"@types/react\": \"^17.0.24\",\n    \"@types/react-dom\": \"^17.0.9\",\n    \"@types/ua-parser-js\": \"^0.7.36\",\n    \"concurrently\": \"^7.0.0\",\n    \"esbuild-visualizer\": \"^0.3.1\",\n    \"jest\": \"^27.4.7\",\n    \"miniflare\": \"^2.11.0\",\n    \"patch-package\": \"^6.4.7\",\n    \"rimraf\": \"^3.0.2\",\n    \"tailwindcss\": \"^3.0.22\",\n    \"ts-jest\": \"^27.1.3\",\n    \"typescript\": \"^4.6.x\",\n    \"wrangler\": \"^2.4.3\"\n  },\n  \"engines\": {\n    \"node\": \">=14\"\n  },\n  \"jest\": {\n    \"preset\": \"ts-jest\",\n    \"testEnvironment\": \"node\",\n    \"coverageReporters\": [\n      \"json-summary\",\n      \"text\",\n      \"lcov\"\n    ],\n    \"moduleNameMapper\": {\n      \"^~/(.*)$\": \"<rootDir>/app/$1\",\n      \"^lodash-es$\": \"lodash\"\n    },\n    \"globals\": {\n      \"ts-jest\": {\n        \"tsconfig\": \"tsconfig.json\"\n      }\n    },\n    \"globalSetup\": \"./tests/setup.js\"\n  },\n  \"sideEffects\": false,\n  \"main\": \"dist/worker.js\"\n}\n"
  },
  {
    "path": "remix.config.js",
    "content": "/**\n * @type {import('@remix-run/dev/config').AppConfig}\n */\nmodule.exports = {\n  appDirectory: \"app\",\n  assetsBuildDirectory: \"public/build\",\n  publicPath: \"/build/\",\n  serverBuildTarget: \"cloudflare-workers\",\n  serverBuildPath: \"build/index.js\",\n  devServerBroadcastDelay: 1000,\n  ignoredRouteFiles: [\".*\"],\n};\n"
  },
  {
    "path": "remix.env.d.ts",
    "content": "/// <reference types=\"@remix-run/dev\" />\n/// <reference types=\"@remix-run/cloudflare-workers/globals\" />\n/// <reference types=\"@cloudflare/workers-types\" />\n"
  },
  {
    "path": "styles/tailwind.css",
    "content": "@import url(\"https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;700;900&family=Roboto+Mono&display=swap\");\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer utilities {\n  @variants responsive {\n    /* Hide scrollbar for Chrome, Safari and Opera */\n    .no-scrollbar::-webkit-scrollbar {\n      display: none;\n    }\n\n    /* Hide scrollbar for IE, Edge and Firefox */\n    .no-scrollbar {\n      -ms-overflow-style: none; /* IE and Edge */\n      scrollbar-width: none; /* Firefox */\n    }\n  }\n}\n\n/* Custom Scrollbar Styles */\n.custom-scrollbar::-webkit-scrollbar {\n  width: 0.75rem;\n}\n\n.custom-scrollbar::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.custom-scrollbar::-webkit-scrollbar-thumb {\n  background: #334155;\n  border-radius: 0.5rem;\n  border: 2px solid #0f172a;\n}\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "module.exports = {\n  content: [\"./app/**/*.{js,ts,jsx,tsx}\"],\n  darkMode: \"class\",\n  theme: {\n    fontSize: {\n      tiny: \".5rem\", // 8px\n      xs: \".625rem\", // 10px\n      sm: \".75rem\", // 12px (SmallBody)\n      base: \".875rem\", // 14px (Body)           <p>\n      lg: \"1rem\", // 16px (SmallTitle)          <h3>\n      xl: \"1.125rem\", // 18px (Title)           <h2>\n      \"2xl\": \"1.375rem\", // 22px (LargeTitle)   <h1>\n      \"3xl\": \"1.5rem\", // 24px\n      \"4xl\": \"1.875rem\", // 30px\n      \"5xl\": \"2rem\", // 32px\n      \"6xl\": [\"2.625rem\", \"3rem\"], // 42px\n      \"7xl\": \"4rem\", // 64px\n      \"8xl\": \"8rem\", // 128px\n    },\n    extend: {\n      height: {\n        viewerHeight: \"calc(100vh - 146px)\",\n        inspectorHeight: \"calc(100vh - 70px)\",\n        jsonViewerHeight: \"calc(100vh - 106px)\",\n        viewerHeightMinimal: \"calc(100vh - 106px)\",\n        inspectorHeightMinimal: \"calc(100vh - 30px)\",\n        jsonViewerHeightMinimal: \"calc(100vh - 66px)\",\n      },\n      fontFamily: {\n        sans: [\"Source Sans Pro\", \"sans-serif\"],\n        mono: [\"Roboto Mono\", \"monospace\"],\n      },\n    },\n  },\n  variants: {\n    outline: [\"focus\"],\n  },\n  plugins: [require(\"@tailwindcss/forms\"), require(\"tailwindcss-radix\")()],\n};\n"
  },
  {
    "path": "tests/formatStarCount.test.ts",
    "content": "import { formatStarCount } from \"../app/utilities/formatStarCount\";\n\ndescribe(\"formatStarCount\", () => {\n  test(\"formats the star count correctly\", () => {\n    expect(formatStarCount(undefined)).toBe(\"⭐️\");\n    expect(formatStarCount(0)).toBe(\"0\");\n    expect(formatStarCount(999)).toBe(\"999\");\n    expect(formatStarCount(1000)).toBe(\"1k\");\n    expect(formatStarCount(1050)).toBe(\"1.1k\");\n    expect(formatStarCount(1100)).toBe(\"1.1k\");\n    expect(formatStarCount(1200)).toBe(\"1.2k\");\n    expect(formatStarCount(1300)).toBe(\"1.3k\");\n    expect(formatStarCount(10000)).toBe(\"10k\");\n    expect(formatStarCount(10050)).toBe(\"10.1k\");\n    expect(formatStarCount(10100)).toBe(\"10.1k\");\n    expect(formatStarCount(10101)).toBe(\"10.1k\");\n    expect(formatStarCount(10199)).toBe(\"10.2k\");\n    expect(formatStarCount(10200)).toBe(\"10.2k\");\n    expect(formatStarCount(52678)).toBe(\"52.7k\");\n    expect(formatStarCount(99949)).toBe(\"99.9k\");\n    expect(formatStarCount(100000)).toBe(\"100k\");\n    expect(formatStarCount(100100)).toBe(\"100.1k\");\n    expect(formatStarCount(101100)).toBe(\"101.1k\");\n  });\n});\n"
  },
  {
    "path": "tests/jsonColumnView.test.ts",
    "content": "import { generateColumnViewNode } from \"../app/utilities/jsonColumnView\";\n\ndescribe(\"generateColumnViewNode\", () => {\n  test(\"it creates the correct tree structure for the passed in JSON\", () => {\n    const json = {\n      string: \"foo bar\",\n      data: { foo: \"bar\" },\n      array: [\n        {\n          string: \"foo bar\",\n        },\n      ],\n    };\n\n    expect(generateColumnViewNode(json)).toMatchInlineSnapshot(`\nObject {\n  \"children\": Array [\n    Object {\n      \"children\": Array [],\n      \"icon\": [Function],\n      \"id\": \"$.string\",\n      \"name\": \"string\",\n      \"subtitle\": \"foo bar\",\n      \"title\": \"string\",\n    },\n    Object {\n      \"children\": Array [\n        Object {\n          \"children\": Array [],\n          \"icon\": [Function],\n          \"id\": \"$.data.foo\",\n          \"name\": \"foo\",\n          \"subtitle\": \"bar\",\n          \"title\": \"foo\",\n        },\n      ],\n      \"icon\": [Function],\n      \"id\": \"$.data\",\n      \"name\": \"data\",\n      \"subtitle\": \"1 field\",\n      \"title\": \"data\",\n    },\n    Object {\n      \"children\": Array [\n        Object {\n          \"children\": Array [\n            Object {\n              \"children\": Array [],\n              \"icon\": [Function],\n              \"id\": \"$.array.0.string\",\n              \"name\": \"string\",\n              \"subtitle\": \"foo bar\",\n              \"title\": \"string\",\n            },\n          ],\n          \"icon\": [Function],\n          \"id\": \"$.array.0\",\n          \"longTitle\": \"Index 0\",\n          \"name\": \"0\",\n          \"subtitle\": \"1 field\",\n          \"title\": \"0\",\n        },\n      ],\n      \"icon\": [Function],\n      \"id\": \"$.array\",\n      \"name\": \"array\",\n      \"subtitle\": \"1 item\",\n      \"title\": \"array\",\n    },\n  ],\n  \"icon\": [Function],\n  \"id\": \"$\",\n  \"name\": \"root\",\n  \"title\": \"root\",\n}\n`);\n  });\n});\n"
  },
  {
    "path": "tests/relatedValues.test.ts",
    "content": "import { calculateRelatedValuesGroups } from \"../app/utilities/relatedValues\";\n\nconst json = {\n  data: [\n    {\n      stream_key: \"831b5bde-cd8a-5bc4-115d-4ba34b19f481\",\n      status: \"idle\",\n      reconnect_window: 60,\n      playback_ids: [\n        {\n          policy: \"public\",\n          id: \"HNRDuwff3K2VjTZZAPuvd2Kx6D01XUQFv02GFBHPUka018\",\n        },\n        {\n          policy: \"public\",\n          id: \"8c836496-d923-4075-9974-3fd40dd74b03\",\n        },\n      ],\n      new_asset_settings: {\n        playback_policies: [\"public\"],\n      },\n      id: \"ZEBrNTpHC02iUah025KM3te6ylM7W4S4silsrFtUkn3Ag\",\n      created_at: \"1609937654\",\n      modifiedAt: \"1609937654\",\n    },\n    {\n      stream_key: \"d273c65e-1fc8-27dc-e9ef-56144cbceb3a\",\n      status: \"idle\",\n      reconnect_window: 60,\n      recent_asset_ids: [\n        \"SZs02xxHgYdkHp00OSCjJiHUHqzVQZNU332XPXRxe341o\",\n        \"e4J9cwb5tjVxMeeV8201dC00i800ThPKKGT2SEN002dHH2s\",\n      ],\n      playback_ids: [\n        {\n          policy: \"public\",\n          id: \"00zOcribkUmXqXHzBTpflk2771BRTcKATqPjWf7JHpuM\",\n        },\n        {\n          policy: \"private\",\n          id: \"00zOcribkUmXqXHzBTpflk2771BRTcKATqPjWf7JHpuM\",\n        },\n      ],\n      new_asset_settings: {\n        playback_policies: [\"public\"],\n      },\n      id: \"B65hEUWW01ErVKDDGImKcBquYhwEAkjW6Ic3lPY0299Cc\",\n      created_at: \"1607587513\",\n      another_field: {\n        nested: \"value\",\n      },\n      modifiedAt: null,\n    },\n    {\n      stream_key: \"6a46c87d-00db-42a9-bad0-aad2872983bf\",\n      status: \"playing\",\n      reconnect_window: 60,\n      recent_asset_ids: [\n        \"SZs02xxHgYdkHp00OSCjJiHUHqzVQZNU332XPXRxe341o\",\n        \"e4J9cwb5tjVxMeeV8201dC00i800ThPKKGT2SEN002dHH2s\",\n      ],\n      playback_ids: [\n        {\n          policy: \"private\",\n          id: \"00zOcribkUmXqXHzBTpflk2771BRTcKATqPjWf7JHpuM\",\n        },\n        {\n          policy: \"internal\",\n          id: \"4b2aceab-f61d-499d-97d7-3f407cbcfbb6\",\n        },\n        {\n          policy: { name: \"shared\", url: \"https://example.com\" },\n          id: \"89952227-ee16-4e4e-b15f-65c127931bcc\",\n        },\n      ],\n      new_asset_settings: {\n        playback_policies: [\"private\"],\n      },\n      id: \"B65hEUWW01ErVKDDGImKcBquYhwEAkjW6Ic3lPY0299Cc\",\n      created_at: \"1607587513\",\n      another_field: {\n        nested: \"value\",\n      },\n    },\n  ],\n};\n\ndescribe(\"calculateRelatedValuesGroups\", () => {\n  test(\"it should return the correct values when path is an object\", () => {\n    const path = \"$.data.1.another_field\";\n\n    const result = calculateRelatedValuesGroups(path, json);\n\n    expect(result).toMatchInlineSnapshot(`\nArray [\n  Object {\n    \"paths\": Array [\n      \"$.data.1.another_field\",\n      \"$.data.2.another_field\",\n    ],\n    \"value\": \"{...}\",\n  },\n  Object {\n    \"paths\": Array [\n      \"$.data.0.another_field\",\n    ],\n    \"value\": \"undefined\",\n  },\n]\n`);\n  });\n\n  test(\"it should return the correct values when path is an array\", () => {\n    const path = \"$.data.1.recent_asset_ids\";\n\n    const result = calculateRelatedValuesGroups(path, json);\n\n    expect(result).toMatchInlineSnapshot(`\nArray [\n  Object {\n    \"paths\": Array [\n      \"$.data.1.recent_asset_ids\",\n      \"$.data.2.recent_asset_ids\",\n    ],\n    \"value\": \"Array(2)\",\n  },\n  Object {\n    \"paths\": Array [\n      \"$.data.0.recent_asset_ids\",\n    ],\n    \"value\": \"undefined\",\n  },\n]\n`);\n  });\n\n  test(\"it should return the correct related values grouped by value\", () => {\n    const path = \"$.data.1.playback_ids.0.policy\";\n\n    const result = calculateRelatedValuesGroups(path, json);\n\n    expect(result).toStrictEqual([\n      {\n        value: \"public\",\n        paths: [\n          \"$.data.0.playback_ids.0.policy\",\n          \"$.data.0.playback_ids.1.policy\",\n          \"$.data.1.playback_ids.0.policy\",\n        ],\n      },\n      {\n        value: \"private\",\n        paths: [\n          \"$.data.1.playback_ids.1.policy\",\n          \"$.data.2.playback_ids.0.policy\",\n        ],\n      },\n      {\n        value: \"internal\",\n        paths: [\"$.data.2.playback_ids.1.policy\"],\n      },\n      {\n        value: \"{...}\",\n        paths: [\"$.data.2.playback_ids.2.policy\"],\n      },\n    ]);\n  });\n\n  test(\"it should group undefined separately from null\", () => {\n    const path = \"$.data.1.modifiedAt\";\n\n    const result = calculateRelatedValuesGroups(path, json);\n\n    console.log(result);\n\n    expect(result).toStrictEqual([\n      {\n        value: \"1609937654\",\n        paths: [\"$.data.0.modifiedAt\"],\n      },\n      {\n        value: \"null\",\n        paths: [\"$.data.1.modifiedAt\"],\n      },\n      {\n        value: \"undefined\",\n        paths: [\"$.data.2.modifiedAt\"],\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "tests/search.test.ts",
    "content": "import { getComponentSlices, getStringSlices } from \"../app/utilities/search\";\n\ndescribe(\"Timezones\", () => {\n  it(\"should always be UTC\", () => {\n    expect(new Date().getTimezoneOffset()).toBe(0);\n  });\n});\n\ndescribe(\"getComponentSlices\", () => {\n  it(\"returns the correct slices for a path that DOES go above the maxWeight even without any matches\", () => {\n    const slices = getComponentSlices(\n      \"records.0.users.9.addresses.0.street_address.street_name\",\n      [],\n      60\n    );\n\n    expect(slices).toMatchInlineSnapshot(`\nArray [\n  Object {\n    \"componentIndex\": 0,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"records\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"componentIndex\": 1,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"0\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"componentIndex\": 2,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"users\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"type\": \"ellipsis\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"componentIndex\": 7,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"street_name\",\n    },\n    \"type\": \"component\",\n  },\n]\n`);\n  });\n\n  it(\"returns the correct slices for a path that DOES go above the maxWeight\", () => {\n    const slices = getComponentSlices(\n      \"records.0.users.9.addresses.0.street_address.street_name\",\n      [\n        { start: 0, end: 2 },\n        { start: 11, end: 15 },\n        { start: 30, end: 36 },\n        { start: 45, end: 51 },\n      ],\n      60\n    );\n\n    expect(slices).toMatchInlineSnapshot(`\nArray [\n  Object {\n    \"componentIndex\": 0,\n    \"slice\": Object {\n      \"isMatch\": true,\n      \"slice\": \"re\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"componentIndex\": 0,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"cords\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"type\": \"ellipsis\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"componentIndex\": 6,\n    \"slice\": Object {\n      \"isMatch\": true,\n      \"slice\": \"street\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"componentIndex\": 6,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"_address\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"componentIndex\": 7,\n    \"slice\": Object {\n      \"isMatch\": true,\n      \"slice\": \"street\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"componentIndex\": 7,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"_name\",\n    },\n    \"type\": \"component\",\n  },\n]\n`);\n  });\n\n  it(\"returns the correct slices for a path that does not go above the maxWeight\", () => {\n    const slices = getComponentSlices(\n      \"records.0.users\",\n      [{ start: 0, end: 4 }],\n      70\n    );\n\n    expect(slices).toMatchInlineSnapshot(`\nArray [\n  Object {\n    \"componentIndex\": 0,\n    \"slice\": Object {\n      \"isMatch\": true,\n      \"slice\": \"reco\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"componentIndex\": 0,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"rds\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"componentIndex\": 1,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"0\",\n    },\n    \"type\": \"component\",\n  },\n  Object {\n    \"type\": \"join\",\n  },\n  Object {\n    \"componentIndex\": 2,\n    \"slice\": Object {\n      \"isMatch\": false,\n      \"slice\": \"users\",\n    },\n    \"type\": \"component\",\n  },\n]\n`);\n  });\n});\n\ndescribe(\"getStringSlices\", () => {\n  it(\"returns a slice for each part of the string based on the matches\", () => {\n    const slices = getStringSlices(\n      \"This is a really great (short) string\",\n      [{ start: 10, end: 16 }],\n      60\n    );\n\n    expect(slices).toMatchInlineSnapshot(`\nArray [\n  Object {\n    \"isMatch\": false,\n    \"slice\": \"This is a \",\n  },\n  Object {\n    \"isMatch\": true,\n    \"slice\": \"really\",\n  },\n  Object {\n    \"isMatch\": false,\n    \"slice\": \" great (short) string\",\n  },\n]\n`);\n  });\n\n  it(\"returns a subset of the string when the matched ranges are outside the window\", () => {\n    const slices = getStringSlices(\n      \"This is a very long string and the largest matched range is outside of the window, so we should try and get only slices of the string that focus on the largest match\",\n      [\n        { start: 10, end: 16 },\n        { start: 80, end: 91 },\n        { start: 100, end: 106 },\n      ],\n      60\n    );\n\n    expect(slices).toMatchInlineSnapshot(`\nArray [\n  Object {\n    \"isMatch\": false,\n    \"slice\": \"…\",\n  },\n  Object {\n    \"isMatch\": false,\n    \"slice\": \" is outside of the windo\",\n  },\n  Object {\n    \"isMatch\": true,\n    \"slice\": \"w, so we sh\",\n  },\n  Object {\n    \"isMatch\": false,\n    \"slice\": \"ould try \",\n  },\n  Object {\n    \"isMatch\": true,\n    \"slice\": \"and ge\",\n  },\n  Object {\n    \"isMatch\": false,\n    \"slice\": \"t only sl\",\n  },\n  Object {\n    \"isMatch\": false,\n    \"slice\": \"…\",\n  },\n]\n`);\n\n    const slices2 = getStringSlices(\n      \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\",\n      [\n        { start: 0, end: 4 },\n        { start: 12, end: 17 },\n        { start: 103, end: 109 },\n        { start: 302, end: 308 },\n      ],\n      56\n    );\n\n    expect(slices2).toMatchInlineSnapshot(`\nArray [\n  Object {\n    \"isMatch\": false,\n    \"slice\": \"…\",\n  },\n  Object {\n    \"isMatch\": false,\n    \"slice\": \" incididunt ut labore et \",\n  },\n  Object {\n    \"isMatch\": true,\n    \"slice\": \"dolore\",\n  },\n  Object {\n    \"isMatch\": false,\n    \"slice\": \" magna aliqua. Ut enim ad\",\n  },\n  Object {\n    \"isMatch\": false,\n    \"slice\": \"…\",\n  },\n]\n`);\n  });\n});\n"
  },
  {
    "path": "tests/setup.js",
    "content": "module.exports = async () => {\n  process.env.TZ = 'UTC';\n};"
  },
  {
    "path": "tests/stableJson.test.ts",
    "content": "import { stableJson } from \"../app/utilities/stableJson\";\n\ntest(\"It should order object keys in a similar order as the first object in an array\", () => {\n  const json = {\n    data: [\n      {\n        id: \"1\",\n        name: \"A\",\n        title: \"A Title\",\n        subtitle: \"A Subtitle\",\n      },\n      {\n        title: \"B Title\",\n        name: \"B\",\n        uuid: \"2\",\n        subtitle: \"B Subtitle\",\n        id: \"2\",\n      },\n      {\n        subtitle: \"C Subtitle\",\n        name: \"C\",\n        title: \"C Title\",\n        id: \"3\",\n        extra: 1,\n      },\n    ],\n  };\n\n  expect(JSON.stringify(stableJson(json), null, 2)).toMatchInlineSnapshot(`\n\"{\n  \\\\\"data\\\\\": [\n    {\n      \\\\\"id\\\\\": \\\\\"1\\\\\",\n      \\\\\"name\\\\\": \\\\\"A\\\\\",\n      \\\\\"title\\\\\": \\\\\"A Title\\\\\",\n      \\\\\"subtitle\\\\\": \\\\\"A Subtitle\\\\\"\n    },\n    {\n      \\\\\"name\\\\\": \\\\\"B\\\\\",\n      \\\\\"title\\\\\": \\\\\"B Title\\\\\",\n      \\\\\"uuid\\\\\": \\\\\"2\\\\\",\n      \\\\\"id\\\\\": \\\\\"2\\\\\",\n      \\\\\"subtitle\\\\\": \\\\\"B Subtitle\\\\\"\n    },\n    {\n      \\\\\"id\\\\\": \\\\\"3\\\\\",\n      \\\\\"name\\\\\": \\\\\"C\\\\\",\n      \\\\\"title\\\\\": \\\\\"C Title\\\\\",\n      \\\\\"subtitle\\\\\": \\\\\"C Subtitle\\\\\",\n      \\\\\"extra\\\\\": 1\n    }\n  ]\n}\"\n`);\n});\n\ntest(\"It should order object keys in a similar order as the first object in an array when nested\", () => {\n  const json = {\n    data: [\n      {\n        foo: [1, 2, 3],\n        bar: [\n          { a: 1, b: 2 },\n          { b: 3, a: 4 },\n        ],\n      },\n      {\n        objects: [\n          {\n            id: \"1\",\n            name: \"A\",\n            title: \"A Title\",\n            subtitle: \"A Subtitle\",\n          },\n          {\n            id: \"2\",\n            name: \"B\",\n            title: \"B Title\",\n            subtitle: \"B Subtitle\",\n            uuid: \"2\",\n          },\n          {\n            id: \"3\",\n            name: \"C\",\n            title: \"C Title\",\n            subtitle: \"C Subtitle\",\n            extra: 1,\n          },\n        ],\n        objects2: [\n          {\n            id: \"1\",\n            name: \"A\",\n            title: \"A Title\",\n            subtitle: \"A Subtitle\",\n          },\n          {\n            id: \"2\",\n            name: \"B\",\n            title: \"B Title\",\n            subtitle: \"B Subtitle\",\n            uuid: \"2\",\n          },\n          {\n            id: \"3\",\n            name: \"C\",\n            title: \"C Title\",\n            subtitle: \"C Subtitle\",\n            extra: 1,\n          },\n        ],\n      },\n    ],\n  };\n\n  expect(JSON.stringify(stableJson(json), null, 2)).toMatchInlineSnapshot(`\n\"{\n  \\\\\"data\\\\\": [\n    {\n      \\\\\"foo\\\\\": [\n        1,\n        2,\n        3\n      ],\n      \\\\\"bar\\\\\": [\n        {\n          \\\\\"a\\\\\": 1,\n          \\\\\"b\\\\\": 2\n        },\n        {\n          \\\\\"a\\\\\": 4,\n          \\\\\"b\\\\\": 3\n        }\n      ]\n    },\n    {\n      \\\\\"objects\\\\\": [\n        {\n          \\\\\"id\\\\\": \\\\\"1\\\\\",\n          \\\\\"name\\\\\": \\\\\"A\\\\\",\n          \\\\\"title\\\\\": \\\\\"A Title\\\\\",\n          \\\\\"subtitle\\\\\": \\\\\"A Subtitle\\\\\"\n        },\n        {\n          \\\\\"id\\\\\": \\\\\"2\\\\\",\n          \\\\\"name\\\\\": \\\\\"B\\\\\",\n          \\\\\"title\\\\\": \\\\\"B Title\\\\\",\n          \\\\\"subtitle\\\\\": \\\\\"B Subtitle\\\\\",\n          \\\\\"uuid\\\\\": \\\\\"2\\\\\"\n        },\n        {\n          \\\\\"id\\\\\": \\\\\"3\\\\\",\n          \\\\\"name\\\\\": \\\\\"C\\\\\",\n          \\\\\"title\\\\\": \\\\\"C Title\\\\\",\n          \\\\\"subtitle\\\\\": \\\\\"C Subtitle\\\\\",\n          \\\\\"extra\\\\\": 1\n        }\n      ],\n      \\\\\"objects2\\\\\": [\n        {\n          \\\\\"id\\\\\": \\\\\"1\\\\\",\n          \\\\\"name\\\\\": \\\\\"A\\\\\",\n          \\\\\"title\\\\\": \\\\\"A Title\\\\\",\n          \\\\\"subtitle\\\\\": \\\\\"A Subtitle\\\\\"\n        },\n        {\n          \\\\\"id\\\\\": \\\\\"2\\\\\",\n          \\\\\"name\\\\\": \\\\\"B\\\\\",\n          \\\\\"title\\\\\": \\\\\"B Title\\\\\",\n          \\\\\"subtitle\\\\\": \\\\\"B Subtitle\\\\\",\n          \\\\\"uuid\\\\\": \\\\\"2\\\\\"\n        },\n        {\n          \\\\\"id\\\\\": \\\\\"3\\\\\",\n          \\\\\"name\\\\\": \\\\\"C\\\\\",\n          \\\\\"title\\\\\": \\\\\"C Title\\\\\",\n          \\\\\"subtitle\\\\\": \\\\\"C Subtitle\\\\\",\n          \\\\\"extra\\\\\": 1\n        }\n      ]\n    }\n  ]\n}\"\n`);\n});\n\ntest(\"It should not convert an array to an object in nested arrays\", () => {\n  const json = {\n    data: [\n      [1],\n      [2]\n    ],\n  };\n\n  expect(JSON.stringify(stableJson(json), null, 2)).toMatchInlineSnapshot(`\n\"{\n  \\\\\"data\\\\\": [\n    [\n      1\n    ],\n    [\n      2\n    ]\n  ]\n}\"\n`);\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"include\": [\"remix.env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2019\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2019\",\n    \"strict\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"skipDefaultLibCheck\": true,\n    \"skipLibCheck\": true,\n\n    // Remix takes care of building everything in `remix build`.\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "worker/index.js",
    "content": "import { createEventHandler } from \"@remix-run/cloudflare-workers\";\n\nimport * as build from \"../build\";\n\naddEventListener(\n  \"fetch\",\n  createEventHandler({\n    build,\n    getLoadContext(event) {\n      return {\n        waitUntil(promise) {\n          return event.waitUntil(promise);\n        },\n      };\n    },\n  })\n);\n"
  },
  {
    "path": "wrangler.toml",
    "content": "name = \"jsonhero-io\"\ntype = \"javascript\"\ncompatibility_date = \"2022-02-01\"\n\naccount_id = \"0e405dda228e3448f1366acb0bc341ef\"\nworkers_dev = true\n\nkv_namespaces = [ \n  { binding = \"DOCUMENTS\", id = \"8c6a9a71fc504abdaaa7aa409c7b2abd\" }\n]\n\n[vars]\nGRAPH_JSON_COLLECTION = \"jsonhero-dev\"\n\n[site]\nbucket = \"./public\"\nentry-point = \".\"\n\n[build]\ncommand = \"npm run build:worker\"\nwatch_dir = \"build/index.js\"\n\n[build.upload]\nformat=\"service-worker\"\n\n[env.production]\nroute = \"jsonhero.io/*\"\nzone_id = \"af3c12add61e94b88b3d2cbfb6f65782\"\nkv_namespaces = [ \n  { binding = \"DOCUMENTS\", id = \"e517c369774a443eba95afff9128f7b6\" }\n]\n\n[env.production.vars]\nGRAPH_JSON_COLLECTION = \"jsonhero-prod\"\n\n# Secrets\n# [SESSION_STORAGE]\n# [GRAPH_JSON_API_KEY]\n# [APIHERO_PROJECT_KEY]\n"
  },
  {
    "path": "wrangler.toml.dev",
    "content": "type = \"javascript\"\ncompatibility_date = \"2022-02-01\"\n\naccount_id = \"c974caaaa18255c03f1771097bade662\"\nworkers_dev = true\n\nkv_namespaces = [ \n  { binding = \"DOCUMENTS\", id = \"a90ce96eb26c4989a1232db27e864ed6\" }\n]\n\n[vars]\nGRAPH_JSON_COLLECTION = \"jsonhero-dev\"\n\n[site]\nbucket = \"./public\"\nentry-point = \".\"\n\n[build]\ncommand = \"npm run build:worker\"\nwatch_dir = \"build/index.js\"\n\n[build.upload]\nformat=\"service-worker\"\n\n[env.production]\nkv_namespaces = [ \n  { binding = \"DOCUMENTS\", id = \"a90ce96eb26c4989a1232db27e864ed6\" }\n]\n\n[env.production.vars]\nGRAPH_JSON_COLLECTION = \"jsonhero-prod\"\n\n# Secrets\n# [SESSION_STORAGE]\n# [GRAPH_JSON_API_KEY]\n"
  }
]