[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n  push:\n    branches:\n      - master\n      - main\n\njobs:\n  checks:\n    name: Checks\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.30.0\n\n      - name: Setup Node\n        uses: actions/setup-node@v5\n        with:\n          node-version: 24\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Lint\n        run: pnpm lint\n\n      - name: Typecheck\n        run: pnpm typecheck\n\n      - name: Test\n        run: pnpm test\n\n      - name: Build package and demo\n        run: pnpm build:all\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/**/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/dist\n/lib\n/demo/dist\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"trailingComma\": \"es5\",\n  \"tabWidth\": 2,\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"printWidth\": 100\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"git.ignoreLimitWarning\": true\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Hirad Arshadi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Reoverlay\n\nA tiny, typed modal manager for React. Reoverlay gives you one top-level\n`ModalContainer` and a small imperative API for opening, stacking, and closing\nmodals from anywhere in your app.\n\n[![Version](https://img.shields.io/npm/v/reoverlay)](https://www.npmjs.com/package/reoverlay)\n[![Downloads](https://img.shields.io/npm/dw/reoverlay)](https://www.npmjs.com/package/reoverlay)\n[![License](https://img.shields.io/npm/l/reoverlay)](LICENSE)\n\n## Install\n\n```bash\npnpm add reoverlay\n```\n\n```bash\nnpm install reoverlay\nyarn add reoverlay\n```\n\n## Quick Start\n\nMount `ModalContainer` once near the root of your app.\n\n```tsx\nimport { ModalContainer } from 'reoverlay'\n\nexport function App() {\n  return (\n    <>\n      <Routes />\n      <ModalContainer />\n    </>\n  )\n}\n```\n\nCreate a modal. `ModalWrapper` is optional, but it provides the default backdrop,\nanimations, outside-click close behavior, and Escape close behavior.\n\n```tsx\nimport { ModalWrapper, Reoverlay } from 'reoverlay'\nimport 'reoverlay/ModalWrapper.css'\n\ntype ConfirmModalProps = {\n  message: string\n  onConfirm: () => void\n}\n\nexport function ConfirmModal({ message, onConfirm }: ConfirmModalProps) {\n  return (\n    <ModalWrapper aria-label=\"Confirm action\">\n      <p>{message}</p>\n      <button onClick={onConfirm} type=\"button\">\n        Confirm\n      </button>\n      <button onClick={() => Reoverlay.hideModal()} type=\"button\">\n        Cancel\n      </button>\n    </ModalWrapper>\n  )\n}\n```\n\nOpen the modal directly.\n\n```tsx\nimport { Reoverlay } from 'reoverlay'\nimport { ConfirmModal } from './ConfirmModal'\n\nReoverlay.showModal(ConfirmModal, {\n  message: 'Delete this post?',\n  onConfirm: () => {\n    Reoverlay.hideModal()\n  },\n})\n```\n\n## Named Modals\n\nYou can configure modal names once and open them later by string. This is useful\nfor app-wide modals, interceptors, and places where importing the modal component\nwould be awkward.\n\n```tsx\nimport { ModalContainer, Reoverlay } from 'reoverlay'\nimport { AuthModal, ConfirmModal } from './modals'\n\nReoverlay.config([\n  { name: 'AuthModal', component: AuthModal },\n  { name: 'ConfirmModal', component: ConfirmModal },\n])\n\nexport function App() {\n  return (\n    <>\n      <Routes />\n      <ModalContainer />\n    </>\n  )\n}\n```\n\n```tsx\nReoverlay.showModal('ConfirmModal', {\n  message: 'Archive this item?',\n})\n```\n\n## API\n\n### `Reoverlay.config(configData)`\n\nRegisters named modals.\n\n```ts\ntype ModalConfigItem = {\n  name: string\n  component: React.ElementType | React.ReactElement\n}\n```\n\nNames must be unique within a single config call.\n\n### `Reoverlay.showModal(modal, props?)`\n\nShows a modal. `modal` can be a configured string name, a React component, or a\nReact element. `props` are passed to the modal when it renders.\n\n```tsx\nReoverlay.showModal(MyModal, { title: 'Hello' })\nReoverlay.showModal(<MyModal title=\"Hello\" />)\nReoverlay.showModal('MyModal', { title: 'Hello' })\n```\n\n### `Reoverlay.hideModal(modalName?)`\n\nHides a modal. When no name is provided, the last visible modal is hidden. When a\nname is provided, that configured modal is hidden.\n\n```ts\nReoverlay.hideModal()\nReoverlay.hideModal('ConfirmModal')\n```\n\n### `Reoverlay.hideAll()`\n\nCloses every active modal.\n\n```ts\nReoverlay.hideAll()\n```\n\n## `ModalWrapper`\n\n`ModalWrapper` is a small default shell. You can skip it and render your own\nmodal UI if you only want Reoverlay's state orchestration.\n\n```tsx\nimport type { ModalWrapperProps } from 'reoverlay'\n```\n\n| Prop                        | Type                                                                                                          | Default                       |\n| --------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------- |\n| `animation`                 | `'fade' \\| 'zoom' \\| 'flip' \\| 'door' \\| 'rotate' \\| 'slideUp' \\| 'slideDown' \\| 'slideLeft' \\| 'slideRight'` | `'fade'`                      |\n| `wrapperClassName`          | `string`                                                                                                      | `''`                          |\n| `contentContainerClassName` | `string`                                                                                                      | `''`                          |\n| `onClose`                   | `(event) => void`                                                                                             | `() => Reoverlay.hideModal()` |\n| `closeOnEscape`             | `boolean`                                                                                                     | `true`                        |\n| `aria-label`                | `string`                                                                                                      | `undefined`                   |\n| `aria-labelledby`           | `string`                                                                                                      | `undefined`                   |\n| `aria-describedby`          | `string`                                                                                                      | `undefined`                   |\n| `role`                      | `'dialog' \\| 'alertdialog'`                                                                                   | `'dialog'`                    |\n\nThe preferred CSS import is:\n\n```ts\nimport 'reoverlay/ModalWrapper.css'\n```\n\nThe legacy import path is still supported:\n\n```ts\nimport 'reoverlay/lib/ModalWrapper.css'\n```\n\n## Development\n\nThis repo uses pnpm workspaces.\n\n```bash\npnpm install\npnpm dev\n```\n\nUseful scripts:\n\n```bash\npnpm lint\npnpm typecheck\npnpm test\npnpm build:package\npnpm build:demo\npnpm build:all\n```\n\nThe demo lives in `demo/` and builds with Vite for GitHub Pages under\n`/reoverlay/`.\n\n## Release Checklist\n\n1. Update the version in `package.json`.\n2. Run `pnpm install --frozen-lockfile`.\n3. Run `pnpm lint`, `pnpm typecheck`, `pnpm test`, and `pnpm build:all`.\n4. Inspect the package contents with `npm pack --dry-run`.\n5. Publish with `pnpm publish --otp <code>` if your npm account requires 2FA.\n6. Deploy the demo with `pnpm --filter reoverlay-demo deploy`.\n\n## License\n\n[MIT](LICENSE)\n"
  },
  {
    "path": "demo/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"description\" content=\"Reoverlay demo: a tiny typed modal manager for React.\" />\n    <title>Reoverlay demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "demo/package.json",
    "content": "{\n  \"name\": \"reoverlay-demo\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"deploy\": \"vite build && gh-pages -d dist\",\n    \"dev\": \"vite --host 0.0.0.0\",\n    \"preview\": \"vite preview\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"reoverlay\": \"workspace:*\",\n    \"react\": \"^19.2.5\",\n    \"react-dom\": \"^19.2.5\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-react\": \"^6.0.1\",\n    \"typescript\": \"^6.0.3\",\n    \"vite\": \"^8.0.10\"\n  }\n}\n"
  },
  {
    "path": "demo/src/App.tsx",
    "content": "import { ModalContainer, ModalWrapper, Reoverlay, type ModalAnimation } from 'reoverlay'\nimport 'reoverlay/ModalWrapper.css'\n\nimport logo from './assets/logo.svg'\nimport myPhoto from './assets/me.jpeg'\nimport Icon from './Icon'\n\nconst animationTypes: ModalAnimation[] = [\n  'fade',\n  'zoom',\n  'flip',\n  'door',\n  'rotate',\n  'slideUp',\n  'slideDown',\n  'slideLeft',\n  'slideRight',\n]\n\nconst installationCode = `\npnpm add reoverlay\n// or if you prefer npm\nnpm install reoverlay\n`\n\ntype DemoModalProps = {\n  animation: ModalAnimation\n}\n\nconst Code = ({ code }: { code: string }) => (\n  <div className=\"Code\">\n    <pre>\n      <code>{code.trim()}</code>\n    </pre>\n  </div>\n)\n\nconst Modal3 = ({ animation }: DemoModalProps) => {\n  return (\n    <ModalWrapper animation={animation} contentContainerClassName=\"modalContent\">\n      <h3 className=\"modalContent__title\">\n        #3 Modal. Ok that's enough 😆 (Though you can keep stacking up as you wish.)\n      </h3>\n      <div className=\"modalContent__buttonContainer\">\n        <button onClick={() => Reoverlay.hideModal()} type=\"button\">\n          Hide\n        </button>\n        <button onClick={() => Reoverlay.hideAll()} type=\"button\">\n          Hide all\n        </button>\n      </div>\n    </ModalWrapper>\n  )\n}\n\nconst Modal2 = ({ animation }: DemoModalProps) => {\n  return (\n    <ModalWrapper animation={animation} contentContainerClassName=\"modalContent\">\n      <h3 className=\"modalContent__title\">\n        #2 Modal. It's getting dark here 🌗. Wanna see the third one?\n      </h3>\n      <div className=\"modalContent__buttonContainer\">\n        <button onClick={() => Reoverlay.showModal(Modal3, { animation })} type=\"button\">\n          Heck yeah! Show #3 Modal\n        </button>\n        <button onClick={() => Reoverlay.hideModal()} type=\"button\">\n          Hide\n        </button>\n        <button onClick={() => Reoverlay.hideAll()} type=\"button\">\n          Hide all\n        </button>\n      </div>\n    </ModalWrapper>\n  )\n}\n\nconst Modal1 = ({ animation }: DemoModalProps) => {\n  return (\n    <ModalWrapper animation={animation} contentContainerClassName=\"modalContent\">\n      <h3 className=\"modalContent__title\">#1 Modal. So sweet! ❤️. Wanna see more?</h3>\n      <div className=\"modalContent__buttonContainer\">\n        <button onClick={() => Reoverlay.showModal(Modal2, { animation })} type=\"button\">\n          Yup! Show #2 Modal\n        </button>\n        <button onClick={() => Reoverlay.hideModal()} type=\"button\">\n          Hide\n        </button>\n      </div>\n    </ModalWrapper>\n  )\n}\n\nconst App = () => {\n  const showModal = (animation: ModalAnimation) => {\n    Reoverlay.showModal(Modal1, { animation })\n  }\n\n  return (\n    <main>\n      <div className=\"container\">\n        <header className=\"header\">\n          <img className=\"header__logo\" src={logo} alt=\"Reoverlay\" />\n          <h1 className=\"header__title\">Reoverlay</h1>\n          <p className=\"header__description\">The missing solution for managing modals in React.</p>\n          <div className=\"header__buttonContainer\">\n            <a\n              className=\"header__githubButton\"\n              href=\"https://github.com/hiradary/reoverlay\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <span className=\"header__githubButtonText\">Visit GitHub</span>\n              <Icon name=\"github\" />\n            </a>\n            <a\n              className=\"header__donationButton\"\n              href=\"https://www.buymeacoffee.com/hiradary\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <span className=\"header__donationButtonText\">Buy me a coffee</span>\n              <Icon name=\"donation\" />\n            </a>\n          </div>\n\n          <Code code={installationCode} />\n        </header>\n\n        <section className=\"section\">\n          <h3 className=\"section__title\">Animation types</h3>\n          <p className=\"section__description\">\n            There are quite a few preset animations. You can create your own custom animation too!\n          </p>\n\n          <div className=\"section__animationTypesContainer\">\n            {animationTypes.map((type) => (\n              <button\n                className=\"section__animationTypeContainer\"\n                key={type}\n                onClick={() => showModal(type)}\n                type=\"button\"\n              >\n                <span className=\"section__animationType\">{type}</span>\n              </button>\n            ))}\n          </div>\n        </section>\n\n        <section className=\"section\">\n          <h3 className=\"section__title\">Usage, Props, etc.</h3>\n          <p className=\"section__description\">\n            You can find more information on{' '}\n            <a\n              href=\"https://github.com/hiradary/reoverlay\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"section__link\"\n            >\n              github\n            </a>\n            .\n          </p>\n        </section>\n\n        <footer className=\"footer\">\n          <img src={myPhoto} alt=\"Hirad Arshadi\" className=\"footer__profilePhoto\" />\n          <p className=\"footer__intentionText\">\n            Made with{' '}\n            <span role=\"img\" aria-label=\"Love\">\n              ❤️\n            </span>{' '}\n            for the react community\n          </p>\n          <a\n            href=\"https://twitter.com/hiradary\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"footer__twitterContainer\"\n          >\n            <Icon name=\"twitter\" className=\"footer__twitterIcon\" />\n            <span className=\"footer__twitterHandle\">@hiradary</span>\n          </a>\n        </footer>\n      </div>\n      <ModalContainer />\n    </main>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "demo/src/Icon.tsx",
    "content": "type IconProps = {\n  className?: string\n  name: 'donation' | 'github' | 'twitter'\n}\n\nconst Icon = ({ className, name }: IconProps) => {\n  switch (name) {\n    case 'github':\n      return (\n        <svg className={className} width=\"19\" height=\"19\" viewBox=\"0 0 19 19\" fill=\"none\">\n          <path\n            clipRule=\"evenodd\"\n            d=\"M9.5 0.5C4.25204 0.5 0 4.62997 0 9.72726C0 13.8026 2.72175 17.2629 6.49721 18.4832C6.97379 18.5677 7.14638 18.2848 7.14638 18.0387C7.14638 17.8188 7.13767 17.239 7.1345 16.4701C4.49033 17.026 3.93379 15.2329 3.93379 15.2329C3.50075 14.1663 2.87929 13.8811 2.87929 13.8811C2.01638 13.3105 2.94421 13.3228 2.94421 13.3228C3.89658 13.3889 4.39771 14.2717 4.39771 14.2717C5.24479 15.6835 6.62071 15.2752 7.16221 15.0406C7.24929 14.4432 7.49629 14.0372 7.76546 13.8065C5.65646 13.575 3.439 12.7823 3.439 9.24668C3.439 8.23706 3.81029 7.4143 4.41592 6.76839C4.32013 6.53771 3.99237 5.59729 4.50854 4.32701C4.50854 4.32701 5.30654 4.08095 7.12183 5.2728C7.89702 5.0682 8.6967 4.96425 9.5 4.96369C10.3075 4.96676 11.1205 5.06826 11.8782 5.2728C13.6935 4.08095 14.4883 4.32701 14.4883 4.32701C15.0076 5.59729 14.683 6.53771 14.5833 6.76839C15.1929 7.4143 15.5578 8.23706 15.5578 9.24668C15.5578 12.7915 13.338 13.5689 11.2227 13.8003C11.5631 14.0825 11.8655 14.6469 11.8655 15.5066C11.8655 16.7408 11.8568 17.735 11.8568 18.0387C11.8568 18.2848 12.0262 18.5731 12.5091 18.4832C16.2814 17.2606 19 13.8026 19 9.72726C19 4.62997 14.748 0.5 9.5 0.5Z\"\n            fill=\"white\"\n            fillRule=\"evenodd\"\n          />\n        </svg>\n      )\n    case 'donation':\n      return (\n        <svg className={className} width=\"16\" height=\"23\" viewBox=\"0 0 16 23\" fill=\"none\">\n          <path\n            clipRule=\"evenodd\"\n            d=\"M8.00672 5.12561L2.55713 5.09277L5.23327 22H5.81715H11.1694H11.7533L14.4295 5.09277L8.00672 5.12561Z\"\n            fill=\"#1A273C\"\n            fillRule=\"evenodd\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M6.54918 5.03302L1.5 5L3.97951 22H4.52049H9.47951H10.0205L12.5 5L6.54918 5.03302Z\"\n            fill=\"#435B81\"\n            fillRule=\"evenodd\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M8.18518 5.03302L3 5L5.5463 22H6.10185H9.89815H10.4537L13 5L8.18518 5.03302Z\"\n            fill=\"#263B5D\"\n            fillRule=\"evenodd\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M1 5.09286H15.0619V3.53369H1V5.09286Z\"\n            fill=\"white\"\n            fillRule=\"evenodd\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M1 5.09286H15.0619V3.53369H1V5.09286Z\"\n            fillRule=\"evenodd\"\n            stroke=\"black\"\n            strokeWidth=\"0.729856\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M12.3371 1H8.97979H6.98485H3.62751L2.60571 3.33875H6.98485H8.97979H13.3589L12.3371 1Z\"\n            fill=\"white\"\n            fillRule=\"evenodd\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M12.3371 1H8.97979H6.98485H3.62751L2.60571 3.33875H6.98485H8.97979H13.3589L12.3371 1Z\"\n            fillRule=\"evenodd\"\n            stroke=\"#050505\"\n            strokeWidth=\"0.729856\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M8.00672 5.12561L1.58398 5.09277L4.26012 22H4.84401H11.1694H11.7533L14.4294 5.09277L8.00672 5.12561Z\"\n            fillRule=\"evenodd\"\n            stroke=\"black\"\n            strokeWidth=\"0.729856\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M14.6242 9.86768H8.21309H7.75192H1.34082L2.54002 16.5916L7.98251 16.533L13.425 16.5916L14.6242 9.86768Z\"\n            fill=\"white\"\n            fillRule=\"evenodd\"\n          />\n          <path\n            clipRule=\"evenodd\"\n            d=\"M14.6242 9.86768H8.21309H7.75192H1.34082L2.54002 16.5916L7.98251 16.533L13.425 16.5916L14.6242 9.86768Z\"\n            fillRule=\"evenodd\"\n            stroke=\"black\"\n            strokeWidth=\"0.729856\"\n          />\n        </svg>\n      )\n    case 'twitter':\n      return (\n        <svg className={className} width=\"24\" height=\"24\" viewBox=\"0 0 112.197 112.197\">\n          <circle fill=\"#2578FF\" cx=\"56.099\" cy=\"56.098\" r=\"56.098\" />\n          <path\n            fill=\"#F1F2F2\"\n            d=\"M90.461,40.316c-2.404,1.066-4.99,1.787-7.702,2.109c2.769-1.659,4.894-4.284,5.897-7.417\n            c-2.591,1.537-5.462,2.652-8.515,3.253c-2.446-2.605-5.931-4.233-9.79-4.233c-7.404,0-13.409,6.005-13.409,13.409\n            c0,1.051,0.119,2.074,0.349,3.056c-11.144-0.559-21.025-5.897-27.639-14.012c-1.154,1.98-1.816,4.285-1.816,6.742\n            c0,4.651,2.369,8.757,5.965,11.161c-2.197-0.069-4.266-0.672-6.073-1.679c-0.001,0.057-0.001,0.114-0.001,0.17\n            c0,6.497,4.624,11.916,10.757,13.147c-1.124,0.308-2.311,0.471-3.532,0.471c-0.866,0-1.705-0.083-2.523-0.239\n            c1.706,5.326,6.657,9.203,12.526,9.312c-4.59,3.597-10.371,5.74-16.655,5.74c-1.08,0-2.15-0.063-3.197-0.188\n            c5.931,3.806,12.981,6.025,20.553,6.025c24.664,0,38.152-20.432,38.152-38.153c0-0.581-0.013-1.16-0.039-1.734\n            C86.391,45.366,88.664,43.005,90.461,40.316L90.461,40.316z\"\n          />\n        </svg>\n      )\n  }\n}\n\nexport default Icon\n"
  },
  {
    "path": "demo/src/main.tsx",
    "content": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\n\nimport App from './App'\nimport './styles.css'\n\ncreateRoot(document.getElementById('root') as HTMLElement).render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n)\n"
  },
  {
    "path": "demo/src/styles.css",
    "content": "@font-face {\n  font-family: 'ProductSans';\n  font-weight: 400;\n  src:\n    local('Product Sans'),\n    local('ProductSans-Regular'),\n    url('./fonts/ProductSansRegular.woff2') format('woff2');\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'ProductSans';\n  font-weight: 700;\n  src:\n    local('Product Sans Bold'),\n    local('ProductSans-Bold'),\n    url('./fonts/ProductSansBold.woff2') format('woff2');\n  font-style: normal;\n}\n\n:root {\n  --color-blue: #2578ff;\n  --color-blue-dark-primary: #263b5d;\n  --color-blue-dark-secondary: #435b81;\n  --color-gray: #b1b8c5;\n  --color-gray-light: rgba(177, 184, 197, 0.15);\n  --size-huge-title: 2em;\n  --size-title: 1.5625em;\n}\n\nhtml,\nbody,\ndiv,\nspan,\napplet,\nobject,\niframe,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\np,\nblockquote,\npre,\na,\nabbr,\nacronym,\naddress,\nbig,\ncite,\ncode,\ndel,\ndfn,\nem,\nimg,\nins,\nkbd,\nq,\ns,\nsamp,\nsmall,\nstrike,\nstrong,\nsub,\nsup,\ntt,\nvar,\nb,\nu,\ni,\ncenter,\ndl,\ndt,\ndd,\nol,\nul,\nli,\nfieldset,\nform,\nlabel,\nlegend,\ntable,\ncaption,\ntbody,\ntfoot,\nthead,\ntr,\nth,\ntd,\narticle,\naside,\ncanvas,\ndetails,\nembed,\nfigure,\nfigcaption,\nfooter,\nheader,\nhgroup,\nmenu,\nnav,\noutput,\nruby,\nsection,\nsummary,\ntime,\nmark,\naudio,\nvideo {\n  margin: 0;\n  padding: 0;\n  border: 0;\n  font-size: 100%;\n  font: inherit;\n  vertical-align: baseline;\n}\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmenu,\nnav,\nsection {\n  display: block;\n}\n\nbody {\n  line-height: 1;\n}\n\nol,\nul {\n  list-style: none;\n}\n\nblockquote,\nq {\n  quotes: none;\n}\n\nblockquote::before,\nblockquote::after,\nq::before,\nq::after {\n  content: '';\n}\n\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: 'ProductSans', sans-serif;\n  font-weight: normal;\n}\n\na {\n  -webkit-tap-highlight-color: transparent;\n  color: inherit;\n  text-decoration: none;\n}\n\nbutton {\n  -webkit-tap-highlight-color: transparent;\n  border: none;\n  outline: none;\n  cursor: pointer;\n  font-family: inherit;\n}\n\nmain {\n  width: 100%;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 20vh 0;\n}\n\n.container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n  max-width: 40em;\n}\n\n.header {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n}\n\n.header .header__logo {\n  width: 221px;\n  height: 144px;\n}\n\n.header .header__title {\n  color: var(--color-blue-dark-primary);\n  font-size: var(--size-huge-title);\n  font-weight: bold;\n  padding-top: 0.5em;\n}\n\n.header .header__description {\n  color: var(--color-gray);\n  padding-top: 1.5em;\n  text-align: center;\n  line-height: 1.5em;\n}\n\n.header .header__buttonContainer {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 2.625em 0 1.5em;\n}\n\n.header .header__githubButton {\n  background-color: var(--color-blue);\n  height: 3.75em;\n  padding: 0 3em;\n  border-radius: 1em;\n  color: white;\n  transition: box-shadow 0.35s;\n  user-select: none;\n  margin: 0 1em;\n  display: flex;\n  align-items: center;\n}\n\n.header .header__githubButtonText {\n  padding-right: 0.5em;\n  white-space: nowrap;\n}\n\n.header .header__githubButton:hover {\n  box-shadow: 0 8px 15px rgba(37, 120, 255, 0.2);\n}\n\n.header .header__donationButton {\n  border: 1px solid #d7dee9;\n  height: 3.75em;\n  padding: 0 2.5em;\n  border-radius: 1em;\n  margin: 0 1em;\n  display: flex;\n  align-items: center;\n  color: var(--color-blue-dark-primary);\n  user-select: none;\n  transition: box-shadow 0.35s;\n}\n\n.header .header__donationButtonText {\n  padding-right: 0.5em;\n  white-space: nowrap;\n}\n\n.Code {\n  width: 100%;\n}\n\n.Code pre {\n  overflow: auto;\n  border-radius: 1em;\n  background: #f6f8fb;\n  color: var(--color-blue-dark-primary);\n  line-height: 1.5;\n  padding: 1.5em;\n}\n\n.section {\n  margin-top: 3.75em;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n.section .section__title {\n  color: var(--color-blue-dark-primary);\n  font-size: var(--size-title);\n  font-weight: bold;\n  margin-bottom: 0.2em;\n  text-align: left;\n}\n\n.section .section__description {\n  color: var(--color-gray);\n  line-height: 1.5em;\n}\n\n.section .section__description .section__link {\n  color: var(--color-blue-dark-secondary);\n}\n\n.section .section__animationTypesContainer {\n  width: 100%;\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-between;\n  padding-top: 1.5em;\n}\n\n.section .section__animationTypeContainer {\n  width: 28%;\n  margin-bottom: 2em;\n  background: transparent;\n  padding: 0;\n}\n\n.section .section__animationType {\n  background-color: var(--color-gray-light);\n  border-radius: 1em;\n  text-align: center;\n  padding: 1em 0;\n  color: var(--color-blue-dark-secondary);\n  user-select: none;\n  cursor: pointer;\n  -webkit-tap-highlight-color: transparent;\n  display: block;\n}\n\n.modalContent {\n  padding: 2em 5em;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.modalContent .modalContent__title {\n  font-weight: bold;\n  font-size: 1.5em;\n}\n\n.modalContent .modalContent__buttonContainer {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  margin-top: 2em;\n}\n\n.modalContent .modalContent__buttonContainer button {\n  border: 1px solid #d7dee9;\n  padding: 1em 2.5em;\n  border-radius: 1em;\n  margin: 0 1em;\n  display: flex;\n  align-items: center;\n  color: var(--color-blue-dark-primary);\n  user-select: none;\n  background-color: white;\n  transition: 0.3s background-color;\n}\n\n.modalContent .modalContent__buttonContainer button:hover {\n  background-color: var(--color-gray-light);\n}\n\n.footer {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n  margin-top: 3.75em;\n}\n\n.footer .footer__profilePhoto {\n  width: 6.25em;\n  height: 6.25em;\n  border-radius: 100%;\n  margin-bottom: 1.25em;\n}\n\n.footer .footer__intentionText {\n  color: var(--color-gray);\n}\n\n.footer .footer__twitterIcon {\n  width: 1em;\n}\n\n.footer .footer__twitterContainer {\n  display: flex;\n  align-items: center;\n  margin-top: 1em;\n}\n\n.footer .footer__twitterHandle {\n  padding-left: 0.2em;\n  color: var(--color-blue-dark-primary);\n}\n\n@media (max-width: 768px) {\n  .container {\n    padding: 0 1.25em;\n  }\n\n  .modalContent {\n    padding: 2em 3em;\n  }\n\n  .modalContent .modalContent__buttonContainer {\n    flex-direction: column;\n  }\n\n  .modalContent .modalContent__buttonContainer button:not(:last-child) {\n    margin-bottom: 1em;\n  }\n}\n\n@media (max-width: 500px) {\n  .header .header__buttonContainer {\n    flex-direction: column;\n  }\n\n  .header .header__githubButton {\n    margin: 0 0 1em;\n  }\n}\n"
  },
  {
    "path": "demo/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"allowImportingTsExtensions\": true,\n    \"noEmit\": true,\n    \"paths\": {\n      \"reoverlay\": [\"../src/index.ts\"],\n      \"reoverlay/ModalWrapper.css\": [\"../src/ModalWrapper.css\"]\n    },\n    \"types\": [\"vite/client\"]\n  },\n  \"include\": [\"src\", \"vite.config.ts\"]\n}\n"
  },
  {
    "path": "demo/vite.config.ts",
    "content": "import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nconst root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')\n\nexport default defineConfig({\n  base: '/reoverlay/',\n  plugins: [react()],\n  resolve: {\n    alias: [\n      {\n        find: /^reoverlay\\/ModalWrapper.css$/,\n        replacement: path.resolve(root, 'src/ModalWrapper.css'),\n      },\n      {\n        find: 'reoverlay',\n        replacement: path.resolve(root, 'src/index.ts'),\n      },\n    ],\n  },\n})\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import js from '@eslint/js'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config(\n  {\n    ignores: ['coverage', 'dist', 'lib', 'demo/dist'],\n  },\n  js.configs.recommended,\n  ...tseslint.configs.recommended,\n  {\n    files: ['**/*.{ts,tsx}'],\n    languageOptions: {\n      parserOptions: {\n        projectService: true,\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n    rules: {\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/no-empty-object-type': 'off',\n    },\n  }\n)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"reoverlay\",\n  \"version\": \"1.1.0\",\n  \"description\": \"A tiny, typed modal manager for React.\",\n  \"license\": \"MIT\",\n  \"author\": \"Hirad Arshadi <hiradwork@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/hiradary/reoverlay\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/hiradary/reoverlay/issues\"\n  },\n  \"homepage\": \"https://hiradary.github.io/reoverlay\",\n  \"keywords\": [\n    \"modal\",\n    \"overlay\",\n    \"react\",\n    \"react-modal\",\n    \"react-overlay\",\n    \"reoverlay\"\n  ],\n  \"packageManager\": \"pnpm@10.30.0\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    },\n    \"./ModalWrapper.css\": \"./dist/ModalWrapper.css\",\n    \"./lib/ModalWrapper.css\": \"./lib/ModalWrapper.css\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"files\": [\n    \"dist\",\n    \"lib\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"sideEffects\": [\n    \"**/*.css\"\n  ],\n  \"scripts\": {\n    \"build\": \"pnpm build:package\",\n    \"build:all\": \"pnpm build:package && pnpm build:demo\",\n    \"build:demo\": \"pnpm --filter reoverlay-demo build\",\n    \"build:package\": \"tsup\",\n    \"clean\": \"rm -rf dist lib coverage demo/dist\",\n    \"dev\": \"pnpm --filter reoverlay-demo dev\",\n    \"format\": \"prettier --write .\",\n    \"lint\": \"eslint .\",\n    \"prepublishOnly\": \"pnpm lint && pnpm typecheck && pnpm test && pnpm build:package\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit && pnpm --filter reoverlay-demo typecheck\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8.0\",\n    \"react-dom\": \">=16.8.0\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^10.0.1\",\n    \"@testing-library/jest-dom\": \"^6.9.1\",\n    \"@testing-library/react\": \"^16.3.2\",\n    \"@types/node\": \"^25.6.0\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@vitejs/plugin-react\": \"^6.0.1\",\n    \"eslint\": \"^10.2.1\",\n    \"gh-pages\": \"^6.3.0\",\n    \"jsdom\": \"^29.0.2\",\n    \"prettier\": \"^3.8.3\",\n    \"react\": \"^19.2.5\",\n    \"react-dom\": \"^19.2.5\",\n    \"tsup\": \"^8.5.1\",\n    \"typescript\": \"^6.0.3\",\n    \"typescript-eslint\": \"^8.59.0\",\n    \"vite\": \"^8.0.10\",\n    \"vitest\": \"^4.1.5\"\n  }\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - demo\n"
  },
  {
    "path": "src/ModalContainer.tsx",
    "content": "import { Fragment, cloneElement, isValidElement, useEffect, useState } from 'react'\n\nimport { EVENT } from './constants'\nimport type { ActiveModal } from './types'\nimport { eventManager } from './utils'\n\nconst ModalContainer = () => {\n  const [modals, setModals] = useState<ActiveModal[]>([])\n\n  useEffect(() => {\n    const unsubscribe = eventManager.on<ActiveModal[]>(EVENT.CHANGE_MODAL, setModals)\n    return unsubscribe\n  }, [])\n\n  return (\n    <div className=\"reOverlay\">\n      {modals.map(({ modalKey, component, props }) => {\n        if (isValidElement(component)) {\n          return <Fragment key={`id-${modalKey}`}>{cloneElement(component, props)}</Fragment>\n        }\n\n        const Component = component\n\n        return (\n          <Fragment key={`id-${modalKey}`}>\n            <Component {...props} />\n          </Fragment>\n        )\n      })}\n    </div>\n  )\n}\n\nexport default ModalContainer\n"
  },
  {
    "path": "src/ModalWrapper.css",
    "content": ".reOverlay .reOverlay__modalWrapper {\n  width: 100%;\n  height: 100vh;\n  height: 100dvh;\n  position: fixed;\n  top: 0;\n  left: 0;\n  z-index: 999;\n  background-color: rgba(0, 0, 0, 0.6);\n  display: -webkit-box;\n  display: -ms-flexbox;\n  display: flex;\n  -webkit-box-align: center;\n  -ms-flex-align: center;\n  align-items: center;\n  -webkit-box-pack: center;\n  -ms-flex-pack: center;\n  justify-content: center;\n  -webkit-animation: ro-fade 0.3s forwards;\n  animation: ro-fade 0.3s forwards;\n}\n\n.reOverlay .reOverlay__modalWrapper,\n.reOverlay .reOverlay__modalWrapper * {\n  box-sizing: border-box;\n}\n\n.reOverlay .reOverlay__modalWrapper .reOverlay__modalContainer {\n  background-color: white;\n}\n\n.reOverlay .reOverlay__modalWrapper.-ro-zoom .reOverlay__modalContainer {\n  -webkit-animation: ro-zoom 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n  animation: ro-zoom 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n}\n\n.reOverlay .reOverlay__modalWrapper.-ro-slideDown .reOverlay__modalContainer {\n  -webkit-animation: ro-slideDown 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n  animation: ro-slideDown 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n}\n\n.reOverlay .reOverlay__modalWrapper.-ro-slideUp .reOverlay__modalContainer {\n  -webkit-animation: ro-slideUp 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n  animation: ro-slideUp 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n}\n\n.reOverlay .reOverlay__modalWrapper.-ro-slideLeft .reOverlay__modalContainer {\n  -webkit-animation: ro-slideLeft 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n  animation: ro-slideLeft 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n}\n\n.reOverlay .reOverlay__modalWrapper.-ro-slideRight .reOverlay__modalContainer {\n  -webkit-animation: ro-slideRight 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n  animation: ro-slideRight 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n}\n\n.reOverlay .reOverlay__modalWrapper.-ro-flip .reOverlay__modalContainer {\n  -webkit-animation: ro-flip 0.3s forwards ease-in;\n  animation: ro-flip 0.3s forwards ease-in;\n  -webkit-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n}\n\n.reOverlay .reOverlay__modalWrapper.-ro-rotate .reOverlay__modalContainer {\n  -webkit-animation: ro-rotate 0.3s forwards ease-out;\n  animation: ro-rotate 0.3s forwards ease-out;\n  -webkit-transform-origin: center;\n  -ms-transform-origin: center;\n  transform-origin: center;\n}\n\n.reOverlay .reOverlay__modalWrapper.-ro-door .reOverlay__modalContainer {\n  -webkit-animation: ro-door 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n  animation: ro-door 0.3s cubic-bezier(0.4, 0, 0, 1.5) forwards;\n}\n\n/* Keyframes */\n\n@-webkit-keyframes ro-fade {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes ro-fade {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n@-webkit-keyframes ro-zoom {\n  from {\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n@keyframes ro-zoom {\n  from {\n    -webkit-transform: scale3d(0.3, 0.3, 0.3);\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n@-webkit-keyframes ro-slideDown {\n  from {\n    -webkit-transform: translate3d(0, -2rem, 0);\n    transform: translate3d(0, -2rem, 0);\n  }\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@keyframes ro-slideDown {\n  from {\n    -webkit-transform: translate3d(0, -2rem, 0);\n    transform: translate3d(0, -2rem, 0);\n  }\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@-webkit-keyframes ro-slideUp {\n  from {\n    -webkit-transform: translate3d(0, 2rem, 0);\n    transform: translate3d(0, 2rem, 0);\n  }\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@keyframes ro-slideUp {\n  from {\n    -webkit-transform: translate3d(0, 2rem, 0);\n    transform: translate3d(0, 2rem, 0);\n  }\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@-webkit-keyframes ro-slideLeft {\n  from {\n    -webkit-transform: translate3d(-2rem, 0, 0);\n    transform: translate3d(-2rem, 0, 0);\n  }\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@keyframes ro-slideLeft {\n  from {\n    -webkit-transform: translate3d(-2rem, 0, 0);\n    transform: translate3d(-2rem, 0, 0);\n  }\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@-webkit-keyframes ro-slideRight {\n  from {\n    -webkit-transform: translate3d(2rem, 0, 0);\n    transform: translate3d(2rem, 0, 0);\n  }\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@keyframes ro-slideRight {\n  from {\n    -webkit-transform: translate3d(2rem, 0, 0);\n    transform: translate3d(2rem, 0, 0);\n  }\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@-webkit-keyframes ro-flip {\n  from {\n    -webkit-transform: perspective(18rem) rotate3d(1, 0, 0, 50deg);\n    transform: perspective(18rem) rotate3d(1, 0, 0, 50deg);\n  }\n  70% {\n    -webkit-transform: perspective(18rem) rotate3d(1, 0, 0, -15deg);\n    transform: perspective(18rem) rotate3d(1, 0, 0, -15deg);\n  }\n  to {\n    -webkit-transform: perspective(18rem);\n    transform: perspective(18rem);\n  }\n}\n\n@keyframes ro-flip {\n  from {\n    -webkit-transform: perspective(18rem) rotate3d(1, 0, 0, 50deg);\n    transform: perspective(18rem) rotate3d(1, 0, 0, 50deg);\n  }\n  70% {\n    -webkit-transform: perspective(18rem) rotate3d(1, 0, 0, -15deg);\n    transform: perspective(18rem) rotate3d(1, 0, 0, -15deg);\n  }\n  to {\n    -webkit-transform: perspective(18rem);\n    transform: perspective(18rem);\n  }\n}\n\n@-webkit-keyframes ro-rotate {\n  from {\n    -webkit-transform: rotate3d(0, 0, 1, -180deg) scale3d(0.3, 0.3, 0.3);\n    transform: rotate3d(0, 0, 1, -180deg) scale3d(0.3, 0.3, 0.3);\n  }\n  to {\n    -webkit-transform: rotate3d(0, 0, 1, 0deg) scale3d(1, 1, 1);\n    transform: rotate3d(0, 0, 1, 0deg) scale3d(1, 1, 1);\n  }\n}\n\n@keyframes ro-rotate {\n  from {\n    -webkit-transform: rotate3d(0, 0, 1, -180deg) scale3d(0.3, 0.3, 0.3);\n    transform: rotate3d(0, 0, 1, -180deg) scale3d(0.3, 0.3, 0.3);\n  }\n  to {\n    -webkit-transform: rotate3d(0, 0, 1, 0deg) scale3d(1, 1, 1);\n    transform: rotate3d(0, 0, 1, 0deg) scale3d(1, 1, 1);\n  }\n}\n\n@-webkit-keyframes ro-door {\n  from {\n    -webkit-transform: scale3d(0, 1, 1);\n    transform: scale3d(0, 1, 1);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n@keyframes ro-door {\n  from {\n    -webkit-transform: scale3d(0, 1, 1);\n    transform: scale3d(0, 1, 1);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .reOverlay .reOverlay__modalWrapper,\n  .reOverlay .reOverlay__modalWrapper .reOverlay__modalContainer {\n    -webkit-animation-duration: 1ms;\n    animation-duration: 1ms;\n  }\n}\n"
  },
  {
    "path": "src/ModalWrapper.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport type React from 'react'\n\nimport Reoverlay from './Reoverlay'\nimport type { ModalWrapperProps } from './types'\n\nconst ModalWrapper = ({\n  'aria-describedby': ariaDescribedBy,\n  'aria-label': ariaLabel,\n  'aria-labelledby': ariaLabelledBy,\n  animation = 'fade',\n  children = null,\n  closeOnEscape = true,\n  contentContainerClassName = '',\n  onClose = () => {\n    Reoverlay.hideModal()\n  },\n  role = 'dialog',\n  wrapperClassName = '',\n}: ModalWrapperProps) => {\n  const wrapperElement = useRef<HTMLDivElement>(null)\n\n  useEffect(() => {\n    if (!closeOnEscape) return undefined\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === 'Escape') {\n        onClose(event)\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [closeOnEscape, onClose])\n\n  const handleClickOutside = (event: React.MouseEvent<HTMLDivElement>) => {\n    if (event.target === wrapperElement.current) {\n      onClose(event)\n    }\n  }\n\n  return (\n    <div\n      aria-describedby={ariaDescribedBy}\n      aria-label={ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      aria-modal=\"true\"\n      className={`reOverlay__modalWrapper -ro-${animation} ${wrapperClassName}`.trim()}\n      onClick={handleClickOutside}\n      ref={wrapperElement}\n      role={role}\n    >\n      <div className={`reOverlay__modalContainer ${contentContainerClassName}`.trim()}>\n        {children}\n      </div>\n    </div>\n  )\n}\n\nexport default ModalWrapper\n"
  },
  {
    "path": "src/Reoverlay.ts",
    "content": "import { EVENT, VALIDATE } from './constants'\nimport type { ActiveModal, ModalConfigItem, ModalProps, ModalRenderable } from './types'\nimport { eventManager, getLastElement, validate } from './utils'\n\ntype ModalSnapshot<P extends ModalProps = ModalProps> = Omit<ActiveModal<P>, 'modalKey'>\n\nlet modalId = 0\n\nconst createModalKey = () => {\n  modalId += 1\n  return `reoverlay-${modalId}`\n}\n\nconst Reoverlay = {\n  modals: new Map<string, ModalRenderable>(),\n  snapshots: new Map<string, ModalSnapshot<any>>(),\n\n  config(configData: ModalConfigItem[] = []) {\n    validate(VALIDATE.CONFIG, configData)\n\n    configData.forEach((item) => {\n      this.modals.set(item.name, item.component)\n    })\n  },\n\n  showModal<P extends ModalProps = ModalProps>(\n    modal: ModalRenderable<P> | string,\n    props = {} as P\n  ) {\n    const modalType = validate(VALIDATE.SHOW_MODAL, modal)\n\n    if (modalType === 'string') {\n      const modalKey = modal as string\n      const modalElement = this.modals.get(modalKey)\n\n      if (!modalElement) {\n        throw new Error(\n          `Reoverlay: Modal not found. Make sure \"${modalKey}\" has been passed to Reoverlay.config().`\n        )\n      }\n\n      this.applyModal({\n        component: modalElement,\n        modalKey,\n        props,\n        type: EVENT.SHOW_MODAL,\n      })\n      return\n    }\n\n    this.applyModal({\n      component: modal as ModalRenderable<P>,\n      modalKey: createModalKey(),\n      props,\n      type: EVENT.SHOW_MODAL,\n    })\n  },\n\n  getSnapshotsArray() {\n    return Array.from(this.snapshots.entries()).map(([modalKey, value]) => ({\n      modalKey,\n      ...value,\n    }))\n  },\n\n  hideModal(modal: string | null = null) {\n    if (modal) {\n      validate(VALIDATE.HIDE_MODAL, modal)\n\n      const modalKey = modal\n      const snapshot = this.snapshots.get(modalKey)\n\n      if (!snapshot) {\n        throw new Error(\"Reoverlay: Snapshot not found. You're trying to hide a missing modal.\")\n      }\n\n      this.applyModal({\n        ...snapshot,\n        modalKey,\n        type: EVENT.HIDE_MODAL,\n      })\n      return\n    }\n\n    const lastSnapshot = getLastElement(this.getSnapshotsArray()) ?? null\n\n    if (lastSnapshot) {\n      this.applyModal({ ...lastSnapshot, type: EVENT.HIDE_MODAL })\n      return\n    }\n\n    console.error(\"Reoverlay: There's no active modal to be hidden.\")\n  },\n\n  hideAll() {\n    this.applyModal({ type: EVENT.HIDE_ALL })\n  },\n\n  applyModal({\n    component,\n    modalKey,\n    props,\n    type,\n  }: Partial<ActiveModal<any>> & { type: (typeof EVENT)[keyof typeof EVENT] }) {\n    switch (type) {\n      case EVENT.SHOW_MODAL:\n        if (component && modalKey) {\n          this.snapshots.set(modalKey, { component, props: props ?? {} })\n        }\n        break\n      case EVENT.HIDE_ALL:\n        this.snapshots.clear()\n        break\n      default:\n        if (modalKey) {\n          this.snapshots.delete(modalKey)\n        }\n        break\n    }\n\n    eventManager.emit(EVENT.CHANGE_MODAL, this.getSnapshotsArray())\n  },\n}\n\nexport default Reoverlay\n"
  },
  {
    "path": "src/constants/index.ts",
    "content": "export const VALIDATE = {\n  CONFIG: 'config',\n  HIDE_MODAL: 'hide_modal',\n  SHOW_MODAL: 'show_modal',\n} as const\n\nexport const EVENT = {\n  CHANGE_MODAL: 'change_modal',\n  HIDE_ALL: 'hide_all',\n  HIDE_MODAL: 'hide_modal',\n  SHOW_MODAL: 'show_modal',\n} as const\n"
  },
  {
    "path": "src/index.ts",
    "content": "export { default as ModalContainer } from './ModalContainer'\nexport { default as ModalWrapper } from './ModalWrapper'\nexport { default as Reoverlay } from './Reoverlay'\nexport type {\n  ActiveModal,\n  ModalAnimation,\n  ModalCloseEvent,\n  ModalComponent,\n  ModalConfigItem,\n  ModalElement,\n  ModalProps,\n  ModalRenderable,\n  ModalWrapperProps,\n} from './types'\n"
  },
  {
    "path": "src/types.ts",
    "content": "import type React from 'react'\n\nexport type ModalProps = Record<string, unknown>\n\nexport type ModalComponent<P = any> = React.ElementType<P>\n\nexport type ModalElement<P = any> = React.ReactElement<P>\n\nexport type ModalRenderable<P = any> = ModalComponent<P> | ModalElement<P>\n\nexport type ModalConfigItem<P = any> = {\n  name: string\n  component: ModalRenderable<P>\n}\n\nexport type ModalAnimation =\n  | 'fade'\n  | 'zoom'\n  | 'flip'\n  | 'door'\n  | 'rotate'\n  | 'slideUp'\n  | 'slideDown'\n  | 'slideLeft'\n  | 'slideRight'\n\nexport type ModalCloseEvent =\n  | React.MouseEvent<HTMLDivElement>\n  | KeyboardEvent\n  | React.KeyboardEvent<HTMLDivElement>\n\nexport type ModalWrapperProps = {\n  'aria-describedby'?: string\n  'aria-label'?: string\n  'aria-labelledby'?: string\n  animation?: ModalAnimation\n  children?: React.ReactNode\n  closeOnEscape?: boolean\n  contentContainerClassName?: string\n  onClose?: (event: ModalCloseEvent) => void\n  role?: 'dialog' | 'alertdialog'\n  wrapperClassName?: string\n}\n\nexport type ActiveModal<P = ModalProps> = {\n  component: ModalRenderable<P>\n  modalKey: string\n  props: P\n}\n"
  },
  {
    "path": "src/utils/eventManager.ts",
    "content": "type Listener<TPayload> = (payload: TPayload) => void\n\nconst eventManager = (() => {\n  const subscribers = new Map<string, Set<Listener<unknown>>>()\n\n  const on = <TPayload>(eventName: string, callback: Listener<TPayload>) => {\n    if (!subscribers.has(eventName)) {\n      subscribers.set(eventName, new Set())\n    }\n\n    const listeners = subscribers.get(eventName)\n    listeners?.add(callback as Listener<unknown>)\n\n    return () => off(eventName, callback)\n  }\n\n  const off = <TPayload>(eventName?: string, callback?: Listener<TPayload>) => {\n    if (!eventName) {\n      subscribers.clear()\n      return\n    }\n\n    if (!callback) {\n      subscribers.delete(eventName)\n      return\n    }\n\n    const listeners = subscribers.get(eventName)\n    listeners?.delete(callback as Listener<unknown>)\n\n    if (listeners?.size === 0) {\n      subscribers.delete(eventName)\n    }\n  }\n\n  const emit = <TPayload>(eventName: string, payload: TPayload) => {\n    subscribers.get(eventName)?.forEach((callback) => {\n      callback(payload)\n    })\n  }\n\n  const listenerCount = (eventName: string) => subscribers.get(eventName)?.size ?? 0\n\n  return {\n    emit,\n    listenerCount,\n    off,\n    on,\n  }\n})()\n\nexport default eventManager\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "export { default as eventManager } from './eventManager'\nexport * from './utils'\nexport * from './validator'\n"
  },
  {
    "path": "src/utils/utils.ts",
    "content": "export const getLastElement = <TValue>(array: TValue[]) => array[array.length - 1]\n\nexport const isArrayUnique = <TValue>(array: TValue[]) => new Set(array).size === array.length\n\nexport const isString = (value: unknown): value is string =>\n  typeof value === 'string' || value instanceof String\n\nexport const isModalLikeObject = (value: unknown) =>\n  typeof value === 'object' && value !== null && '$$typeof' in value\n"
  },
  {
    "path": "src/utils/validator.ts",
    "content": "import { isValidElement } from 'react'\n\nimport { VALIDATE } from '../constants'\nimport type { ModalConfigItem, ModalRenderable } from '../types'\nimport { isArrayUnique, isModalLikeObject, isString } from './utils'\n\ntype ValidationType = (typeof VALIDATE)[keyof typeof VALIDATE]\n\nexport type ShowModalValidationResult = 'component' | 'string'\n\nconst isRenderableModal = (value: unknown): value is ModalRenderable =>\n  typeof value === 'function' || isValidElement(value) || isModalLikeObject(value)\n\nexport const validate = (\n  type: ValidationType,\n  value: unknown\n): boolean | ShowModalValidationResult => {\n  switch (type) {\n    case VALIDATE.CONFIG: {\n      if (!Array.isArray(value)) {\n        throw new Error(\n          'Reoverlay: Config data must be an array. Pass an array to Reoverlay.config().'\n        )\n      }\n\n      const configData = value as ModalConfigItem[]\n\n      configData.forEach((item) => {\n        if (!item.name || !item.component) {\n          throw new Error(\n            \"Reoverlay: Each config item must contain a 'name' and 'component' property.\"\n          )\n        }\n      })\n\n      const names = configData.map((item) => item.name)\n      if (!isArrayUnique(names)) {\n        throw new Error('Reoverlay: Modal config names must be unique.')\n      }\n\n      return true\n    }\n\n    case VALIDATE.SHOW_MODAL: {\n      const throwError = () => {\n        throw new Error(\n          \"Reoverlay: Method 'showModal' requires a React component, React element, or configured modal name.\"\n        )\n      }\n\n      if (!value) throwError()\n      if (isString(value)) return 'string'\n      if (isRenderableModal(value)) return 'component'\n\n      throwError()\n      return false\n    }\n\n    case VALIDATE.HIDE_MODAL: {\n      if (isString(value)) return true\n\n      throw new Error(\n        `Reoverlay: Method 'hideModal' accepts an optional string modal name, got ${typeof value}.`\n      )\n    }\n\n    default:\n      return false\n  }\n}\n"
  },
  {
    "path": "tests/ModalContainer.test.tsx",
    "content": "import { act, cleanup, render, screen } from '@testing-library/react'\nimport { afterEach, describe, expect, it } from 'vitest'\n\nimport { EVENT } from '../src/constants'\nimport { ModalContainer, Reoverlay } from '../src'\nimport { eventManager } from '../src/utils'\n\nconst TestModal = ({ label }: { label: string }) => <div>{label}</div>\n\nafterEach(() => {\n  act(() => {\n    Reoverlay.hideAll()\n  })\n  cleanup()\n})\n\ndescribe('ModalContainer', () => {\n  it('subscribes once and cleans up the exact change listener', () => {\n    const { unmount } = render(<ModalContainer />)\n\n    expect(eventManager.listenerCount(EVENT.CHANGE_MODAL)).toBe(1)\n\n    act(() => {\n      Reoverlay.showModal(TestModal, { label: 'Stable listener' })\n    })\n\n    expect(screen.getByText('Stable listener')).toBeInTheDocument()\n    expect(eventManager.listenerCount(EVENT.CHANGE_MODAL)).toBe(1)\n\n    unmount()\n\n    expect(eventManager.listenerCount(EVENT.CHANGE_MODAL)).toBe(0)\n  })\n})\n"
  },
  {
    "path": "tests/ModalWrapper.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react'\nimport { describe, expect, it, vi } from 'vitest'\n\nimport { ModalWrapper } from '../src'\n\ndescribe('ModalWrapper', () => {\n  it('adds dialog semantics and custom classes', () => {\n    render(\n      <ModalWrapper\n        aria-label=\"Confirm action\"\n        animation=\"slideUp\"\n        contentContainerClassName=\"custom-content\"\n        wrapperClassName=\"custom-wrapper\"\n      >\n        Content\n      </ModalWrapper>\n    )\n\n    const dialog = screen.getByRole('dialog', { name: 'Confirm action' })\n\n    expect(dialog).toHaveAttribute('aria-modal', 'true')\n    expect(dialog).toHaveClass('-ro-slideUp')\n    expect(dialog).toHaveClass('custom-wrapper')\n    expect(dialog.firstElementChild).toHaveClass('custom-content')\n  })\n\n  it('calls onClose when the wrapper backdrop is clicked', () => {\n    const onClose = vi.fn()\n    render(\n      <ModalWrapper aria-label=\"Close from backdrop\" onClose={onClose}>\n        <button type=\"button\">Inside</button>\n      </ModalWrapper>\n    )\n\n    fireEvent.click(screen.getByRole('dialog', { name: 'Close from backdrop' }))\n    fireEvent.click(screen.getByRole('button', { name: 'Inside' }))\n\n    expect(onClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('calls onClose when Escape is pressed', () => {\n    const onClose = vi.fn()\n    render(\n      <ModalWrapper aria-label=\"Close from keyboard\" onClose={onClose}>\n        Content\n      </ModalWrapper>\n    )\n\n    fireEvent.keyDown(document, { key: 'Escape' })\n\n    expect(onClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('can disable Escape close behavior', () => {\n    const onClose = vi.fn()\n    render(\n      <ModalWrapper aria-label=\"Escape disabled\" closeOnEscape={false} onClose={onClose}>\n        Content\n      </ModalWrapper>\n    )\n\n    fireEvent.keyDown(document, { key: 'Escape' })\n\n    expect(onClose).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "tests/Reoverlay.test.tsx",
    "content": "import { cleanup, render, screen, act } from '@testing-library/react'\nimport { afterEach, describe, expect, it, vi } from 'vitest'\n\nimport { ModalContainer, Reoverlay } from '../src'\n\nconst TestModal = ({ label }: { label: string }) => <div>{label}</div>\n\nafterEach(() => {\n  act(() => {\n    Reoverlay.hideAll()\n  })\n  cleanup()\n  vi.restoreAllMocks()\n})\n\ndescribe('Reoverlay', () => {\n  it('renders a direct modal component with props', () => {\n    render(<ModalContainer />)\n\n    act(() => {\n      Reoverlay.showModal(TestModal, { label: 'Direct modal' })\n    })\n\n    expect(screen.getByText('Direct modal')).toBeInTheDocument()\n  })\n\n  it('renders a configured modal by name', () => {\n    render(<ModalContainer />)\n    Reoverlay.config([{ component: TestModal, name: 'NamedModal' }])\n\n    act(() => {\n      Reoverlay.showModal('NamedModal', { label: 'Named modal' })\n    })\n\n    expect(screen.getByText('Named modal')).toBeInTheDocument()\n  })\n\n  it('supports React elements passed directly', () => {\n    render(<ModalContainer />)\n\n    act(() => {\n      Reoverlay.showModal(<TestModal label=\"Element modal\" />)\n    })\n\n    expect(screen.getByText('Element modal')).toBeInTheDocument()\n  })\n\n  it('hides the last modal by default', () => {\n    render(<ModalContainer />)\n\n    act(() => {\n      Reoverlay.showModal(TestModal, { label: 'First modal' })\n      Reoverlay.showModal(TestModal, { label: 'Second modal' })\n      Reoverlay.hideModal()\n    })\n\n    expect(screen.getByText('First modal')).toBeInTheDocument()\n    expect(screen.queryByText('Second modal')).not.toBeInTheDocument()\n  })\n\n  it('hides a named modal while leaving other modals open', () => {\n    render(<ModalContainer />)\n    Reoverlay.config([\n      { component: TestModal, name: 'FirstNamedModal' },\n      { component: TestModal, name: 'SecondNamedModal' },\n    ])\n\n    act(() => {\n      Reoverlay.showModal('FirstNamedModal', { label: 'First named modal' })\n      Reoverlay.showModal('SecondNamedModal', { label: 'Second named modal' })\n      Reoverlay.hideModal('SecondNamedModal')\n    })\n\n    expect(screen.getByText('First named modal')).toBeInTheDocument()\n    expect(screen.queryByText('Second named modal')).not.toBeInTheDocument()\n  })\n\n  it('hides all active modals', () => {\n    render(<ModalContainer />)\n\n    act(() => {\n      Reoverlay.showModal(TestModal, { label: 'First modal' })\n      Reoverlay.showModal(TestModal, { label: 'Second modal' })\n      Reoverlay.hideAll()\n    })\n\n    expect(screen.queryByText('First modal')).not.toBeInTheDocument()\n    expect(screen.queryByText('Second modal')).not.toBeInTheDocument()\n  })\n\n  it('validates duplicate configured names', () => {\n    expect(() => {\n      Reoverlay.config([\n        { component: TestModal, name: 'DuplicateModal' },\n        { component: TestModal, name: 'DuplicateModal' },\n      ])\n    }).toThrow(/unique/i)\n  })\n\n  it('throws for missing configured modals', () => {\n    expect(() => {\n      Reoverlay.showModal('MissingModal')\n    }).toThrow(/not found/i)\n  })\n})\n"
  },
  {
    "path": "tests/package-smoke.test.ts",
    "content": "import { createRequire } from 'node:module'\nimport { execFileSync } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nimport { beforeAll, describe, expect, it } from 'vitest'\n\nconst require = createRequire(import.meta.url)\nconst root = path.resolve(import.meta.dirname, '..')\n\ndescribe('package output', () => {\n  beforeAll(() => {\n    execFileSync('pnpm', ['build:package'], {\n      cwd: root,\n      stdio: 'inherit',\n    })\n  }, 60_000)\n\n  it('emits ESM, CommonJS, declarations, and CSS compatibility files', async () => {\n    expect(existsSync(path.join(root, 'dist/index.js'))).toBe(true)\n    expect(existsSync(path.join(root, 'dist/index.cjs'))).toBe(true)\n    expect(existsSync(path.join(root, 'dist/index.d.ts'))).toBe(true)\n    expect(existsSync(path.join(root, 'dist/ModalWrapper.css'))).toBe(true)\n    expect(existsSync(path.join(root, 'lib/ModalWrapper.css'))).toBe(true)\n\n    const esm = await import(pathToFileURL(path.join(root, 'dist/index.js')).href)\n    const cjs = require('../dist/index.cjs')\n\n    expect(esm.Reoverlay.showModal).toEqual(expect.any(Function))\n    expect(cjs.ModalContainer).toEqual(expect.any(Function))\n  })\n})\n"
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"emitDeclarationOnly\": false,\n    \"noEmit\": false,\n    \"outDir\": \"dist\",\n    \"types\": []\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"tests\", \"**/*.test.ts\", \"**/*.test.tsx\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"ignoreDeprecations\": \"6.0\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"noEmit\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"target\": \"ES2022\",\n    \"types\": [\"vitest/globals\"]\n  },\n  \"include\": [\n    \"src\",\n    \"tests\",\n    \"tsup.config.ts\",\n    \"vitest.config.ts\",\n    \"vitest.setup.ts\",\n    \"eslint.config.mjs\"\n  ]\n}\n"
  },
  {
    "path": "tsup.config.ts",
    "content": "import { copyFile, mkdir } from 'node:fs/promises'\n\nimport { defineConfig } from 'tsup'\n\nconst copyStyles = async () => {\n  await mkdir('dist', { recursive: true })\n  await mkdir('lib', { recursive: true })\n  await copyFile('src/ModalWrapper.css', 'dist/ModalWrapper.css')\n  await copyFile('src/ModalWrapper.css', 'lib/ModalWrapper.css')\n}\n\nexport default defineConfig({\n  clean: true,\n  dts: true,\n  entry: ['src/index.ts'],\n  external: ['react', 'react-dom'],\n  format: ['esm', 'cjs'],\n  minify: true,\n  onSuccess: copyStyles,\n  outDir: 'dist',\n  outExtension({ format }) {\n    return {\n      js: format === 'cjs' ? '.cjs' : '.js',\n    }\n  },\n  sourcemap: true,\n  splitting: false,\n  target: 'es2020',\n  treeshake: true,\n})\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    environment: 'jsdom',\n    globals: true,\n    setupFiles: ['./vitest.setup.ts'],\n  },\n})\n"
  },
  {
    "path": "vitest.setup.ts",
    "content": "import '@testing-library/jest-dom/vitest'\n"
  }
]