[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:react-hooks/recommended\",\n    \"prettier\"\n  ],\n  \"parser\": \"@typescript-eslint/parser\",\n  \"rules\": {\n    \"array-bracket-spacing\": 0,\n    \"no-trailing-spaces\": 1,\n    \"no-tabs\": 1\n  },\n  \"env\": {\n    \"browser\": true,\n    \"es2021\": true\n  },\n  \"overrides\": [],\n  \"parserOptions\": {\n    \"ecmaVersion\": \"latest\",\n    \"sourceType\": \"module\"\n  },\n  \"plugins\": [\"react-hooks\", \"@typescript-eslint\", \"prettier\"]\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\n.DS_Store\n.cache\n.parcel-cache\n"
  },
  {
    "path": ".npmignore",
    "content": "src\ndemo\ntests\n.babelrc\n.eslintrc.json\n.gitignore\n.nvmrc\n.prettierignore\n.prettierrc\ntsconfig.json"
  },
  {
    "path": ".npmrc",
    "content": "tag-version-prefix=\"\""
  },
  {
    "path": ".nvmrc",
    "content": "18.1.0"
  },
  {
    "path": ".prettierignore",
    "content": "*.md"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"printWidth\": 120,\n  \"semi\": false\n}"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [3.3.5] - 2023-08-12\n- Fix hebavior of `allowPan` and `allowZoom` props (thanks [Robert Brownstein](https://github.com/rbrownstein-bd))\n\n## [3.3.4] - 2023-07-02\n- Expose setZoom and setPos methods\n\n## [3.3.3] - 2023-03-29\n- Add the move method to the ref type object\n\n## [3.3.2] - 2023-03-25\n- Add access to the move method from the ref object\n\n## [3.3.1] - 2023-03-02\n- Build fix\n\n## [3.3.0] - 2023-03-01\n- Added optional ignoredMouseButtons prop (thanks apomelitos)\n\n## [3.2.1] - 2023-02-24\n- Fix double-click zoom target\n\n## [3.2.0] - 2023-02-24\n- Refactor codebase to Typescript and functional components (thanks erickriva)\n- Switch to parcel as build tool (thanks erickriva)\n- Improve performances\n- Added custom prop to disable mouse wheel (thanks JenniferGoijman)\n- Fixed an issue when using SSR (thanks gregorjan)\n- Bump some dependencies\n\n## [3.1.1] - 2022-11-20\n- Bump some dependencies\n\n## [3.1.0] - 2022-09-25\n- Added prop to allow parent movement (thanks SaadTaimoor-TFD)\n- Bump again dev dependencies\n\n## [3.0.4] - 2022-09-24\n- Bump dev dependencies to prevent vulnerabilities\n\n## [3.0.3] - 2022-05-08\n- Fix panning on React 18\n- Improve splitting between lib and demo app\n- Temporary disable unit tests\n\n## [3.0.2] - 2022-04-23\n- Update dependencies\n- Include React 18 as peer dependencies\n\n## [3.0.1] - 2022-04-17\n- Fix zooming on mobile when pan is disabled\n\n## [3.0.0] - 2022-01-24\n- Replace locked prop with allowZoom and allowPan to handle zooming and panning events separately (thanks joshuacerdenia)\n\n## [2.2.0] - 2022-01-11\n- Add a prop `allowTouchEvents` to allow event propagation (thanks fkrauthan)\n\n## [2.1.0] - 2021-12-26\n- Add a prop to lock the component\n\n## [2.0.3] - 2021-07-06\n- Prevent error when component is unmounted but still moving\n- Fix double-tap bug on Safari iOS\n\n## [2.0.2] - 2021-06-23\n- Includes React 17 as peer dependencies\n\n## [2.0.1] - 2021-04-22\n- Use wrapper boundaries instead of specified props\n\n## [2.0.0] - 2021-02-26\n- Update all dependencies\n- Rework on the example page\n- Improve mousewheel zoom\n- Fix and improve unit tests\n- Fix chrome warning during zoom\n- Change some eslint and babel rules\n- Improve reference handling\n\n## [1.1.5] - 2021-02-26\n- Added onPanChange callback method (thanks Frozen-byte)\n\n## [1.1.4] - 2020-12-25\n- Fix calculating absolute position (thanks sbekaert)\n\n## [1.1.3] - 2019-10-09\n- Update some dependencies, clean code\n\n## [1.1.2] - 2018-12-11\n- Remove preventDefault from touchStop event\n\n## [1.1.1] - 2018-09-20\n- Fix another bug on mouse wheel zoom\n\n## [1.1.0] - 2018-09-19\n- Add movement deceleration on mouse up and touch end events\n- Greatly improve example project\n- Fix blur effect on mouse wheel zoom\n\n## [1.0.3] - 2018-08-10\n- Add unit tests using [Intern](https://theintern.io/)\n- Improve performances with translate3d and will-change CSS properties\n- Fix a bug on panning when the element is not centered\n\n## [1.0.2] - 2018-08-08\n- Fix on README documentation\n- Lower React dependencies (v16.0)\n\n## [1.0.1] - 2018-08-08\n- Improve README documentation\n- Add code documentation\n- Add NPM and GitLab CI config files\n- Add License\n- Add animation duration in props\n- Add zoom in and out buttons in example project\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Copyright © 2004-2013 by Internet Systems Consortium, Inc. (“ISC”)\nCopyright © 1995-2003 by Internet Software Consortium\n\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED “AS IS” AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# react-prismazoom\n\n## About\n\nA pan and zoom component for React, using CSS transformations.\n\nDepends only upon prop-types, react and react-dom modules.  \nWorks on both desktop and mobile.\n\nOnline demo [here!](https://sylvaindubus.github.io/react-prismazoom/)\n\n### Zoom features :mag_right:\n* Zoom with the mouse wheel or a two-finger pinch\n* Zoom using double-click or double-tap\n* Zoom on the selected area and center\n\n### Pan features :point_up_2:\n* Pan with the mouse pointer or with one finger when zoomed-in\n* Intuitive panning depending on available space when zoomed-in\n* Adjusts cursor style to indicate in which direction the element can be moved\n\n## Contribution\n\nIf you want to contribute, feel free to send a merge request or open a discussion. Currently, I just have time to maintain the package, but not enough to make big changes or add important features.\n\nAll contributions would be quite appreciated! 😉\n\nAmong changes I would like to apply:\n~~- Migrate to TypeScript~~\n~~- Transform to a functional component (that could help split the code)~~\n- Make motion logic less dependent to React\n- Replace Enzyme with another testing library\n\n## Breaking changes on v3\n\n* The `locked` prop has been replaced by `allowZoom` and `allowPan` to handle zooming and panning events separately\n\n## Breaking changes on v2\n\n* The package now requires React v16.3 or higher (to use react references)\n* The zoom feature through gestures or the mouse wheel got some improvements to react better with all devices. You may need to adjust the `scrollVelocity` property passed to the component to keep the same effect.\n\n\n## Installation\n\n### Install the component\n\n```bash\n$ npm i -D react-prismazoom\n```\n\n### Install the demo\n\nThis project includes a full-featured application demo.\n\nFirst clone the project.\n\nGo to the subfolder:\n```bash\n$ cd demo\n```\n\nThen, install it:\n\n```bash\n$ npm ci\n```\n\nRun the Webpack Dev Server:\n\n```bash\n$ npm start\n```\n\n### Run unit tests\n\n⚠️ There are no unit tests anymore since the previously used library is deprecated and doesn't support React 18. The current test suite needs to be adapted using a different library.\n\n## Usage\n\n### Implementation\n\n```jsx\nimport PrismaZoom from 'react-prismazoom'\n\n<PrismaZoom>\n  <img src=\"my-image.png\" />\n  <p>A text that can be zoomed and dragged</p>\n</PrismaZoom>\n```\n\n### Props\n\n| Name | Type | Default | Description |\n| --- | --- | --- |  --- |\n| className | string | None | Class name to apply on the zoom wrapper. |\n| style | object | None | Style to apply on the zoom wrapper. Note that *transform*, *transition*, *cursor*, *touch-action* and *will-change* cannot be overridden. Example: `style={{backgroundColor: 'red'}}`. |\n| minZoom | number | 1 | Minimum zoom ratio. |\n| maxZoom | number | 5 | Maximum zoom ratio. |\n| scrollVelocity | number | 0.1 | Zoom increment or decrement on each scroll wheel detection. |\n| onZoomChange | function | null | Function called each time the zoom value changes. |\n| onPanChange | function | null | Function called each time the posX or posY value changes (aka images was panned). |\n| animDuration | number | 0.25 | Animation duration (in seconds). |\n| doubleTouchMaxDelay | number | 300 | Max delay between two taps to consider a double tap (in milliseconds). |\n| decelerationDuration | number | 750 | Decelerating movement duration after a mouse up or a touch end event (in milliseconds). |\n| allowZoom | boolean | true | Enable or disable zooming in place.\n| allowPan | boolean | true | Enable or disable panning in place.\n| allowTouchEvents | boolean | false | Enables touch event propagation. |\n| allowParentPanning | boolean | false | When enabled, allows the parent element/page to pan with single-finger touch events as long as zoom = 1. |\n| allowWheel | boolean | true | Enable or disable mouse wheel and touchpad zooming in place |\n| ignoredMouseButtons | number[] | [] | Optional array of ignored mouse buttons allows to prevent panning for specific mouse buttons. By default all mouse buttons are enabled. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button#value) |\n\n**Note:** all props are optional.\n\n### Public Methods\n\nThese functions can be called from parent components.\n\n**zoomIn (value)**\n*Increments the zoom with the given value.*\nParam {value: Number} : Zoom value\n\n**zoomOut (value)**\n*Decrements the zoom with the given value.*\nParam {value: Number} : Zoom value\n\n**zoomToZone (relX, relY, relWidth, relHeight)**\n*Zoom in on the specified zone with the given relative coordinates and dimensions.*\nParam {relX: Number}: Relative X position of the zone left-top corner in pixels\nParam {relY: Number}: Relative Y position of the zone left-top corner in pixels\nParam {relWidth: Number}: Zone width in pixels\nParam {relHeight: Number}: Zone height in pixels\n\n**reset ()**\n*Resets the component to its initial state.*\n\n**getZoom ()**\n*Returns the current zoom value.*\nReturn {Number} : Zone value\n\n## License\n\nReact PrismaZoom is licensed under the ISC license. See the LICENSE.md file for more details.\n"
  },
  {
    "path": "demo/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>react-prismazoom</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"./src/static/favicon-32x32.png\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"./src/static/favicon-16x16.png\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" />\n    <meta name=\"theme-color\" content=\"#000\" />\n    <link href=\"https://fonts.googleapis.com/css2?family=Jost:wght@300;400&display=swap\" rel=\"stylesheet\" />\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <script src=\"./src/index.tsx\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "demo/package.json",
    "content": "{\n  \"name\": \"demo\",\n  \"scripts\": {\n    \"start\": \"parcel index.html --no-cache\",\n    \"build\": \"parcel build index.html --public-url ./\",\n    \"publish\": \"npm run build && gh-pages -d dist\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"devDependencies\": {\n    \"gh-pages\": \"^3.2.3\",\n    \"parcel\": \"^2.8.2\",\n    \"parcel-bundler\": \"^1.12.5\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\"\n  },\n  \"alias\": {\n    \"react\": \"./node_modules/react\"\n  }\n}\n"
  },
  {
    "path": "demo/src/App/App.css",
    "content": "html {\n  font-size: 80%;\n}\n\n@media (min-width: 480px) {\n  html {\n    font-size: 100%;\n  }\n}\n\nbody {\n  min-width: 320px;\n  margin: 0;\n  background-color: #111;\n  font-family: 'Jost', sans-serif;\n}\n\nh1,\nh2 {\n  margin: 0;\n  font-weight: normal;\n}\n\n.App {\n  text-align: center;\n}\n\n.App-logo {\n  animation: App-logo-spin infinite 20s linear;\n  height: 100%;\n}\n\n.App-header {\n  height: 80px;\n  padding: 20px;\n  color: white;\n  background-color: #000;\n}\n\n.App-header h1 {\n  display: inline-block;\n  font-size: 3rem;\n  line-height: 3rem;\n  background-image: linear-gradient(120deg, #155799 50%, #991557);\n  background-clip: text;\n  background-size: 200% 100%;\n  background-position: 100%;\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n}\n\n.App-header h2 {\n  margin-top: 0.5em;\n  font-size: 1rem;\n  color: #ddd;\n}\n\n.App-footer {\n  position: absolute;\n  bottom: 0px;\n  text-align: center;\n  width: 100%;\n}\n\n.App-indicator {\n  display: inline-flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 6px 15px;\n  background-color: #111;\n  color: #fff;\n  border-radius: 4px 4px 0 0;\n}\n\n.App-button {\n  width: 32px;\n  height: 32px;\n  padding: 0;\n  text-align: center;\n  border: none;\n  border-radius: 50%;\n  outline: none;\n  background: none;\n  color: #fff;\n  font-size: 0.75rem;\n  cursor: pointer;\n  vertical-align: middle;\n}\n\n.App-buttonIcon {\n  display: block;\n  fill: currentColor;\n  height: 100%;\n}\n\n.App-wrapper {\n  display: flex;\n  height: calc(100vh - 180px);\n  align-items: center;\n  justify-content: center;\n  overflow: hidden;\n  margin: 15px;\n  position: relative;\n}\n\n.App-zoom {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n.App-image {\n  width: 100%;\n  height: 100%;\n  background-size: cover;\n  background-repeat: no-repeat;\n  background-position: center center;\n}\n\n.App-card {\n  display: none;\n  position: absolute;\n  bottom: 30px;\n  right: 30px;\n  width: 360px;\n  padding: 1.5em;\n  font-weight: 300;\n  text-align: left;\n  background-color: rgba(0, 0, 0, 0.7);\n  color: #fff;\n}\n\n.App-cardHeader {\n  margin-bottom: 1em;\n}\n\n.App-card h3 {\n  margin: 0;\n  font-weight: 400;\n  font-size: 1.5rem;\n}\n\n.App-card p {\n  margin: 0 0 1em 0;\n}\n\n.App-card a,\n.App-card a:visited {\n  color: #61dafb;\n}\n\n.App-zoomLabel {\n  display: inline-block;\n  width: 60px;\n  vertical-align: middle;\n}\n\n@media (min-width: 768px) {\n  .App-wrapper {\n    margin: 30px;\n  }\n  .App-card {\n    display: block;\n  }\n}\n"
  },
  {
    "path": "demo/src/App/index.tsx",
    "content": "import React, { ComponentRef, MouseEvent, useCallback, useRef, useState } from 'react'\n\nimport PrismaZoom from '../../../src'\nimport backgroundOne from './images/radeau-de-la-meduse.jpg'\nimport backgroundTwo from './images/eruption-du-vesuve.jpg'\nimport './App.css'\n\nconst App = () => {\n  const prismaZoom = useRef<ComponentRef<typeof PrismaZoom>>(null)\n  const zoomCounterRef = useRef<HTMLSpanElement>(null)\n\n  const [allowZoom, setAllowZoom] = useState(true)\n  const [allowPan, setAllowPan] = useState(true)\n\n  const onZoomChange = useCallback((zoom: number) => {\n    if (!zoomCounterRef.current) return\n    zoomCounterRef.current.innerText = `${Math.round(zoom * 100)}%`\n  }, [])\n\n  const onClickOnZoomOut = () => {\n    prismaZoom.current?.zoomOut(1)\n  }\n\n  const onClickOnZoomIn = () => {\n    prismaZoom.current?.zoomIn(1)\n  }\n\n  const onClickOnLock = () => {\n    setAllowPan((allowPan) => !allowPan)\n    setAllowZoom((allowZoom) => !allowZoom)\n  }\n\n  const onDoubleClickOnCard = (event: MouseEvent) => {\n    event.preventDefault()\n    event.stopPropagation()\n\n    if (!prismaZoom.current || !event.currentTarget?.parentNode) return\n\n    const zoneRect = event.currentTarget.getBoundingClientRect()\n    const layoutRect = (event.currentTarget.parentNode as Element).getBoundingClientRect()\n\n    const zoom = prismaZoom.current.getZoom()\n\n    if (zoom > 1) {\n      prismaZoom.current?.reset()\n      return\n    }\n\n    const [relX, relY] = [(zoneRect.left - layoutRect.left) / zoom, (zoneRect.top - layoutRect.top) / zoom]\n    const [relWidth, relHeight] = [zoneRect.width / zoom, zoneRect.height / zoom]\n    prismaZoom.current?.zoomToZone(relX, relY, relWidth, relHeight)\n  }\n\n  return (\n    <div className=\"App\">\n      <header className=\"App-header\">\n        <h1>react-prismazoom</h1>\n        <h2>A pan and zoom component for React, using CSS transformations.</h2>\n      </header>\n\n      <section className=\"App-wrapper\">\n        <PrismaZoom className=\"App-zoom\" onZoomChange={onZoomChange} maxZoom={8} minZoom={1} ref={prismaZoom}>\n          <div className=\"App-image\" style={{ backgroundImage: `url(${backgroundOne})` }}></div>\n          <article className=\"App-card\" onDoubleClick={onDoubleClickOnCard}>\n            <header className=\"App-cardHeader\">\n              <h3>The Raft of the Medusa</h3>\n              <span>Théodore Géricault</span>\n            </header>\n            <p>\n              The Raft of the Medusa (French: Le Radeau de la Méduse) – originally titled Scène de Naufrage (Shipwreck\n              Scene) – is an oil painting of 1818–19 by the French Romantic painter and lithographer Théodore Géricault\n              (1791–1824). Completed when the artist was 27, the work has become an icon of French Romanticism.\n            </p>\n            <p>\n              <a href=\"https://en.wikipedia.org/wiki/The_Raft_of_the_Medusa\" target=\"_blank\" rel=\"noreferrer\">\n                Go to Wikipedia.\n              </a>\n            </p>\n            <footer>\n              <strong>Tip: </strong>double-click on this card to zoom. 😉\n            </footer>\n          </article>\n        </PrismaZoom>\n\n        <footer className=\"App-footer\">\n          <div className=\"App-indicator\">\n            <button className=\"App-button\" onClick={onClickOnZoomOut}>\n              <svg className=\"App-buttonIcon\" viewBox=\"0 0 24 24\">\n                <path d=\"M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8zm4-9H8a1 1 0 0 0 0 2h8a1 1 0 0 0 0-2z\" />\n              </svg>\n            </button>\n            <span className=\"App-zoomLabel\" ref={zoomCounterRef}>\n              100%\n            </span>\n            <button className=\"App-button\" onClick={onClickOnZoomIn}>\n              <svg className=\"App-buttonIcon\" viewBox=\"0 0 24 24\">\n                <path d=\"M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8zm4-9h-3V8a1 1 0 0 0-2 0v3H8a1 1 0 0 0 0 2h3v3a1 1 0 0 0 2 0v-3h3a1 1 0 0 0 0-2z\" />\n              </svg>\n            </button>\n          </div>\n        </footer>\n      </section>\n\n      <section className=\"App-wrapper\">\n        <PrismaZoom className=\"App-zoom\" allowZoom={allowZoom} allowPan={allowPan} maxZoom={8}>\n          <div className=\"App-image\" style={{ backgroundImage: `url(${backgroundTwo})` }}></div>\n          <article className=\"App-card\">\n            <header className=\"App-cardHeader\">\n              <h3>Vesuvius in Eruption</h3>\n              <span>Joseph Mallord William Turner</span>\n            </header>\n            <p>\n              The eighteenth-century fascination with volcanoes, and Vesuvius in particular, deepened in the nineteenth\n              century, fuelled by the eruptions of Vesuvius in 1794, 1807, 1819, and 1822.\n            </p>\n          </article>\n        </PrismaZoom>\n\n        <footer className=\"App-footer\">\n          <div className=\"App-indicator\">\n            <button className=\"App-button\" onClick={onClickOnLock}>\n              <svg className=\"App-buttonIcon\" viewBox=\"0 0 24 24\">\n                {!allowPan && !allowZoom ? (\n                  <path d=\"M12 13a1.49 1.49 0 0 0-1 2.61V17a1 1 0 0 0 2 0v-1.39A1.49 1.49 0 0 0 12 13zm5-4V7A5 5 0 0 0 7 7v2a3 3 0 0 0-3 3v7a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3zM9 7a3 3 0 0 1 6 0v2H9zm9 12a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z\" />\n                ) : (\n                  <path d=\"M12 13a1.49 1.49 0 0 0-1 2.61V17a1 1 0 0 0 2 0v-1.39A1.49 1.49 0 0 0 12 13zm5-4H9V7a3 3 0 0 1 5.12-2.13 3.08 3.08 0 0 1 .78 1.38 1 1 0 1 0 1.94-.5 5.09 5.09 0 0 0-1.31-2.29A5 5 0 0 0 7 7v2a3 3 0 0 0-3 3v7a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3zm1 10a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z\" />\n                )}\n              </svg>\n            </button>\n          </div>\n        </footer>\n      </section>\n    </div>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "demo/src/index.d.ts",
    "content": "declare module '*.jpg'\n"
  },
  {
    "path": "demo/src/index.tsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\n\nimport App from './App'\n\nReactDOM.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n  document.getElementById('root')\n)\n"
  },
  {
    "path": "demo/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"removeComments\": true,\n    \"sourceMap\": true,\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "demo/types.d.ts",
    "content": "declare module '*.jpg'\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"react-prismazoom\",\n  \"version\": \"3.3.5\",\n  \"description\": \"A pan and zoom component for React, using CSS transformations.\",\n  \"author\": \"Sylvain Dubus <svn.dbs@gmail.com>\",\n  \"contributors\": [\n    \"Erick Estevão Riva Pramio <erickriva@hotmail.com.br>\"\n  ],\n  \"license\": \"ISC\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/sylvaindubus/react-prismazoom\"\n  },\n  \"keywords\": [\n    \"react\",\n    \"react-component\",\n    \"zoom\",\n    \"pan\",\n    \"drag\",\n    \"pinch-zoom\",\n    \"css3\"\n  ],\n  \"main\": \"./dist/cjs/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/esm/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"npm run build:esm && npm run build:cjs\",\n    \"build:esm\": \"tsc\",\n    \"build:cjs\": \"tsc --module commonjs --outDir dist/cjs\",\n    \"build-watch\": \"npm run build:esm -- -w && npm run build:cjs -- -w\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"lint\": \"eslint ./src/** --fix\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8.0\",\n    \"react-dom\": \">=16.8.0\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.0.26\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.46.0\",\n    \"@typescript-eslint/parser\": \"^5.46.0\",\n    \"eslint\": \"^8.14.0\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-plugin-prettier\": \"^4.0.0\",\n    \"eslint-plugin-react\": \"^7.31.11\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-webpack-plugin\": \"^3.1.1\",\n    \"parcel\": \"^2.8.1\",\n    \"parcel-bundler\": \"^1.12.5\",\n    \"prettier\": \"^2.6.2\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"typescript\": \"^4.9.4\"\n  },\n  \"optionalDependencies\": {\n    \"fsevents\": \"*\"\n  }\n}\n"
  },
  {
    "path": "src/index.test.js",
    "content": "// TODO: Rework all tests using a different library\n\n// import React from 'react'\n// import { mount, configure } from 'enzyme'\n// import { JSDOM } from 'jsdom'\n// import Adapter from '@wojtekmaj/enzyme-adapter-react-17'\n\n// import PrismaZoom from '../../src'\n\n// configure({ adapter: new Adapter() })\n\n// const { describe, it, beforeEach } = intern.getPlugin('interface.bdd')\n// const { expect } = intern.getPlugin('chai')\n\n// const documentHTML = '<!doctype html><html><head></head><body><div></div></body></html>'\n// const jsdom = new JSDOM(documentHTML, { pretendToBeVisual: true })\n// global.window = jsdom.window\n// global.window.matchMedia = () => ({\n//   matches: true,\n// })\n// global.document = jsdom.window.document\n// global.navigator = { userAgent: 'node.js' }\n\n// const [containerWidth, containerHeight] = [1440, 800]\n\n// const mockGetBoudingClientRect = (falseData) => {\n//   window.HTMLElement.prototype.getBoundingClientRect = function () {\n//     if (this.className === 'prismaZoom') {\n//       // Return data for the PrismaZoom element\n//       const data = {\n//         width: 640,\n//         height: 360,\n//         top: 0,\n//         left: 0,\n//         right: 640,\n//         bottom: 360,\n//         ...falseData,\n//       }\n//       return data\n//     } else {\n//       // Return data for the parent element\n//       return {\n//         width: containerWidth,\n//         height: containerHeight,\n//         top: 0,\n//         left: 0,\n//         bottom: containerWidth,\n//         right: containerHeight,\n//       }\n//     }\n//   }\n// }\n\n// describe('components', () => {\n//   describe('PrismaZoom', () => {\n//     const props = {\n//       minZoom: 1,\n//       maxZoom: 5,\n//     }\n//     const component = mount(\n//       <PrismaZoom className=\"prismaZoom\" {...props}>\n//         <div></div>\n//       </PrismaZoom>\n//     )\n//     const instance = component.instance()\n//     const defaultState = instance.state\n\n//     beforeEach(() => {\n//       // Re-initialize default state\n//       component.setState(defaultState)\n\n//       // Override clientWidth and clientHeight getters\n//       Object.defineProperty(document.body, 'clientWidth', {\n//         get: () => containerWidth,\n//         configurable: true,\n//       })\n//       Object.defineProperty(document.body, 'clientHeight', {\n//         get: () => containerHeight,\n//         configurable: true,\n//       })\n//     })\n\n//     it('renders correctly', () => {\n//       expect(component.prop('className')).to.equal('prismaZoom')\n//       expect(component.state('zoom')).to.equal(1)\n//     })\n\n//     describe('getNewPosition', () => {\n//       it('returns initial position if zoom is equal to 1', () => {\n//         expect(instance.getNewPosition(5, 5, 1)).to.eql([0, 0])\n//       })\n\n//       it('returns new position when zoom-in', () => {\n//         mockGetBoudingClientRect()\n//         expect(instance.getNewPosition(20, 20, 1.5)).to.eql([150, 80])\n//       })\n\n//       it('returns new position when zoom-out', () => {\n//         component.setState({ zoom: 1.5, posX: 150, posY: 80 })\n//         expect(instance.getNewPosition(20, 20, 1.25)).to.eql([75, 40])\n//       })\n//     })\n\n//     describe('getLimitedShift', () => {\n//       it('returns 0 if element cannot be panned', () => {\n//         expect(instance.getLimitedShift(10, 0, 1440, 0, 3195)).to.eql(0)\n//         expect(instance.getLimitedShift(-10, 0, 1440, 0, -1760)).to.eql(0)\n//       })\n//       it('returns limited shift if the shift is too high', () => {\n//         expect(instance.getLimitedShift(10, 0, 1440, -5, 3195)).to.eql(5)\n//         expect(instance.getLimitedShift(-10, 0, 1440, -1755, 1445)).to.eql(-5)\n//       })\n//       it('returns current shift if the move is far enough from borders', () => {\n//         expect(instance.getLimitedShift(10, 0, 1440, -1590, 1600)).to.eql(10)\n//         expect(instance.getLimitedShift(-10, 0, 1440, -1590, 1600)).to.eql(-10)\n//       })\n//     })\n\n//     describe('getCursor', () => {\n//       it('returns adapted cursor if element cannot be panned', () => {\n//         expect(instance.getCursor()).to.eql('auto')\n//       })\n//       it('returns adapted cursor if element can only be panned horizontally', () => {\n//         expect(instance.getCursor(true, false)).to.eql('ew-resize')\n//       })\n//       it('returns adapted cursor if element can only be panned vertically', () => {\n//         expect(instance.getCursor(false, true)).to.eql('ns-resize')\n//       })\n//       it('returns adapted cursor if element can be panned on both directions', () => {\n//         expect(instance.getCursor(true, true)).to.eql('move')\n//       })\n//     })\n\n//     describe('fullZoomInOnPosition', () => {\n//       it('zoom-in at the maximum value', () => {\n//         instance.fullZoomInOnPosition(5, 5)\n//         expect(instance.state).to.eql({\n//           zoom: 5,\n//           posX: 1260,\n//           posY: 700,\n//           cursor: 'auto',\n//           transitionDuration: 0.25,\n//         })\n//       })\n//     })\n\n//     describe('move', () => {\n//       it('does not changes position if panning is impossible', () => {\n//         instance.move(20, 20, 0)\n//         expect(instance.state.zoom).to.eql(1)\n//         expect(instance.state.posX).to.eql(0)\n//         expect(instance.state.posY).to.eql(0)\n//         expect(instance.state.cursor).to.eql('auto')\n//       })\n\n//       it('changes position toward bottom-right corner', () => {\n//         mockGetBoudingClientRect({ width: 1920, height: 1920, bottom: 1920, right: 1920 })\n//         component.setState({ zoom: 2, posX: 640, posY: 640 })\n//         instance.move(-20, -20, 0)\n//         expect(instance.state.posX).to.eql(620)\n//         expect(instance.state.posY).to.eql(620)\n//         expect(instance.state.cursor).to.eql('move')\n//       })\n\n//       it('changes position toward left-top corner with a limited shift', () => {\n//         mockGetBoudingClientRect({\n//           width: 1920,\n//           height: 1080,\n//           left: -10,\n//           top: -10,\n//           bottom: 1070,\n//           right: 1910,\n//         })\n//         component.setState({ zoom: 3, posX: 630, posY: 350 })\n//         instance.move(20, 20)\n//         expect(instance.state.posX).to.eql(640)\n//         expect(instance.state.posY).to.eql(360)\n//         expect(instance.state.cursor).to.eql('move')\n//       })\n\n//       it('changes position on X axis only', () => {\n//         mockGetBoudingClientRect({\n//           width: containerWidth * 2,\n//           height: 600,\n//           left: 0,\n//           top: 0,\n//           bottom: 600,\n//           right: containerWidth * 2,\n//         })\n//         component.setState({ zoom: 2, posX: 640, posY: 360 })\n//         instance.move(-20, -20)\n//         expect(instance.state.posX).to.eql(620)\n//         expect(instance.state.posY).to.eql(360)\n//         expect(instance.state.cursor).to.eql('ew-resize')\n//       })\n\n//       it('changes position on Y axis only', () => {\n//         mockGetBoudingClientRect({\n//           width: 600,\n//           height: containerHeight * 2,\n//           left: 0,\n//           top: 0,\n//           bottom: containerHeight * 2,\n//           right: 600,\n//         })\n//         component.setState({ zoom: 2, posX: 640, posY: 350 })\n//         instance.move(-20, -20)\n//         expect(instance.state.posX).to.eql(640)\n//         expect(instance.state.posY).to.eql(330)\n//         expect(instance.state.cursor).to.eql('ns-resize')\n//       })\n//     })\n\n//     describe('zoomIn', () => {\n//       it('increments the zoom value', () => {\n//         instance.zoomIn(3)\n//         expect(component.state('zoom')).to.equal(4)\n//         instance.zoomIn(3)\n//         expect(component.state('zoom')).to.equal(props.maxZoom)\n//       })\n//     })\n\n//     describe('zoomOut', () => {\n//       it('decrements the zoom value', () => {\n//         component.setState({ zoom: props.maxZoom })\n//         instance.zoomOut(3)\n//         expect(component.state('zoom')).to.equal(2)\n//         instance.zoomOut(3)\n//         expect(component.state('zoom')).to.equal(props.minZoom)\n//       })\n//     })\n\n//     describe('zoomToZone', () => {\n//       it('zoom-in on the specified zone', () => {\n//         mockGetBoudingClientRect()\n//         component.setState({ zoom: 1, posX: 640, posY: 360 })\n//         instance.zoomToZone(400, 10, 230, 340)\n//         expect(instance.state).to.eql({\n//           zoom: 2.3529411764705883,\n//           posX: -458.8235294117647,\n//           posY: 0,\n//           cursor: 'auto',\n//           transitionDuration: 0.25,\n//         })\n//       })\n//     })\n\n//     describe('reset', () => {\n//       it('resets the state', () => {\n//         instance.reset()\n//         expect(instance.state).to.eql(defaultState)\n//       })\n//     })\n\n//     describe('getZoom', () => {\n//       it('returns the current zoom value', () => {\n//         component.setState({ zoom: 2 })\n//         expect(instance.getZoom()).to.eql(2)\n//       })\n//     })\n//   })\n// })\n"
  },
  {
    "path": "src/index.tsx",
    "content": "import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'\nimport type { Props, Ref, PositionType, CursorType } from './types'\n\n// Transform translateX ans translateY value property\nconst defaultPos: PositionType = [0, 0]\n\n// Cursor style property\nconst defaultCursor = 'auto'\n\nconst PrismaZoom = forwardRef<Ref, Props>((props, forwardedRef) => {\n  const {\n    children,\n    onPanChange,\n    onZoomChange,\n    minZoom = 1,\n    initialZoom = 1,\n    maxZoom = 5,\n    scrollVelocity = 0.2,\n    animDuration = 0.25,\n    doubleTouchMaxDelay = 300,\n    decelerationDuration = 750,\n    allowZoom = true,\n    allowPan = true,\n    allowTouchEvents = false,\n    allowParentPanning = false,\n    allowWheel = true,\n    ignoredMouseButtons = [],\n    ...divProps\n  } = props\n\n  // Reference to the main element\n  const ref = useRef<HTMLDivElement>(null)\n  // Last request animation frame identifier\n  const lastRequestAnimationIdRef = useRef<number | null>()\n  // Last touch time in milliseconds\n  const lastTouchTimeRef = useRef<number>()\n  // Last double tap time (used to limit multiple double tap) in milliseconds\n  const lastDoubleTapTimeRef = useRef<number>()\n  // Last shifted position\n  const lastShiftRef = useRef<PositionType | null>()\n  // Last calculated distance between two fingers in pixels\n  const lastTouchDistanceRef = useRef<number | null>()\n  // Last cursor position\n  const lastCursorRef = useRef<PositionType | null>()\n  // Last touch position\n  const lastTouchRef = useRef<PositionType | null>()\n  // Current zoom level\n  const zoomRef = useRef(initialZoom)\n  // Current position\n  const posRef = useRef(defaultPos)\n  // Current transition duration\n  const transitionRef = useRef(animDuration)\n\n  const [cursor, setCursor] = useState<CursorType>(defaultCursor)\n\n  const update = useCallback(() => {\n    if (!ref.current) return\n    ref.current.style.transition = `transform ease-out ${transitionRef.current}s`\n    ref.current.style.transform = `translate3d(${posRef.current[0]}px, ${posRef.current[1]}px, 0) scale(${zoomRef.current})`\n  }, []);\n\n  const setZoom = useCallback((zoom: number) => {\n    zoomRef.current = zoom\n    update()\n    if (onZoomChange) {\n      onZoomChange(zoom)\n    }\n  }, [update, onZoomChange]);\n\n  const setPos = useCallback((pos: PositionType) => {\n    posRef.current = pos\n    update()\n    if (onPanChange) {\n      onPanChange({ posX: pos[0], posY: pos[1] })\n    }\n  }, [update, onPanChange]);\n\n  const setTransitionDuration = useCallback((duration: number) => {\n    transitionRef.current = duration\n    update()\n  }, [update]);\n\n  /**\n   * Returns the current zoom value.\n   * @return {Number} Zoom value\n   */\n  const getZoom = useCallback(() => zoomRef.current, []);\n\n  /**\n   * Increments the zoom with the given value.\n   * @param  {Number} value Zoom value\n   */\n  const zoomIn = useCallback((value: number) => {\n    let newPosX = posRef.current[0]\n    let newPosY = posRef.current[1]\n\n    const prevZoom = zoomRef.current\n\n    const newZoom = prevZoom + value < maxZoom ? prevZoom + value : maxZoom\n\n    if (newZoom !== prevZoom) {\n      newPosX = (newPosX * (newZoom - 1)) / (prevZoom > 1 ? prevZoom - 1 : prevZoom)\n      newPosY = (newPosY * (newZoom - 1)) / (prevZoom > 1 ? prevZoom - 1 : prevZoom)\n    }\n\n    setZoom(newZoom)\n    setPos([newPosX, newPosY])\n    setTransitionDuration(animDuration)\n  }, [setZoom, setPos, setTransitionDuration, animDuration, maxZoom]);\n\n  /**\n   * Decrements the zoom with the given value.\n   * @param  {Number} value Zoom value\n   */\n  const zoomOut = useCallback((value: number) => {\n    let newPosX = posRef.current[0]\n    let newPosY = posRef.current[1]\n\n    const prevZoom = zoomRef.current\n\n    const newZoom = prevZoom - value > minZoom ? prevZoom - value : minZoom\n\n    if (newZoom !== prevZoom) {\n      newPosX = (newPosX * (newZoom - 1)) / (prevZoom - 1)\n      newPosY = (newPosY * (newZoom - 1)) / (prevZoom - 1)\n    }\n\n    setZoom(newZoom)\n    setPos([newPosX, newPosY])\n    setTransitionDuration(animDuration)\n  }, [setZoom, setPos, setTransitionDuration, animDuration, minZoom])\n\n  /**\n   * Zoom-in on the specified zone with the given relative coordinates and dimensions.\n   * @param  {Number} relX      Relative X position of the zone left-top corner in pixels\n   * @param  {Number} relY      Relative Y position of the zone left-top corner in pixels\n   * @param  {Number} relWidth  Zone width in pixels\n   * @param  {Number} relHeight Zone height in pixels\n   */\n  const zoomToZone = useCallback((relX: number, relY: number, relWidth: number, relHeight: number) => {\n    if (!ref.current) return\n\n    let newPosX = posRef.current[0]\n    let newPosY = posRef.current[1]\n    const parentRect = (ref.current?.parentNode as HTMLElement).getBoundingClientRect()\n\n    const prevZoom = zoomRef.current\n\n    // Calculate zoom factor to scale the zone\n    const optimalZoomX = parentRect.width / relWidth\n    const optimalZoomY = parentRect.height / relHeight\n    const newZoom = Math.min(optimalZoomX, optimalZoomY, maxZoom)\n\n    // Calculate new position to center the zone\n    const rect = ref.current.getBoundingClientRect()\n    const [centerX, centerY] = [rect.width / prevZoom / 2, rect.height / prevZoom / 2]\n    const [zoneCenterX, zoneCenterY] = [relX + relWidth / 2, relY + relHeight / 2]\n    newPosX = (centerX - zoneCenterX) * newZoom\n    newPosY = (centerY - zoneCenterY) * newZoom\n\n    setZoom(newZoom)\n    setPos([newPosX, newPosY])\n    setTransitionDuration(animDuration)\n  }, [setZoom, setPos, setTransitionDuration, animDuration, maxZoom])\n\n  /**\n   * Calculates new translate positions for CSS transformations.\n   * @param  {Number} x     Relative (rect-based) X position in pixels\n   * @param  {Number} y     Relative (rect-based) Y position in pixels\n   * @param  {Number} zoom  Scale value\n   * @return {Array}        New X and Y positions\n   */\n  const getNewPosition = useCallback((x: number, y: number, newZoom: number): PositionType => {\n    const [prevZoom, prevPosX, prevPosY] = [zoomRef.current, posRef.current[0], posRef.current[1]]\n\n    if (newZoom === 1 || !ref.current) return [0, 0]\n\n    if (newZoom > prevZoom) {\n      // Get container coordinates\n      const rect = ref.current.getBoundingClientRect()\n\n      // Retrieve rectangle dimensions and mouse position\n      const [centerX, centerY] = [rect.width / 2, rect.height / 2]\n      const [relativeX, relativeY] = [x - rect.left - window.pageXOffset, y - rect.top - window.pageYOffset]\n\n      // If we are zooming down, we must try to center to mouse position\n      const [absX, absY] = [(centerX - relativeX) / prevZoom, (centerY - relativeY) / prevZoom]\n      const ratio = newZoom - prevZoom\n      return [prevPosX + absX * ratio, prevPosY + absY * ratio]\n    } else {\n      // If we are zooming down, we shall re-center the element\n      return [(prevPosX * (newZoom - 1)) / (prevZoom - 1), (prevPosY * (newZoom - 1)) / (prevZoom - 1)]\n    }\n  }, [])\n\n  /**\n   * Applies a full-zoom on the specified X and Y positions\n   * @param  {Number} x Relative (rect-based) X position in pixels\n   * @param  {Number} y Relative (rect-based) Y position in pixels\n   */\n  const fullZoomInOnPosition = useCallback((x: number, y: number) => {\n    const zoom = maxZoom\n\n    setPos(getNewPosition(x, y, zoom))\n    setZoom(zoom)\n    setTransitionDuration(animDuration)\n  }, [setPos, setZoom, setTransitionDuration, getNewPosition, animDuration, maxZoom])\n\n  /**\n   * Calculates the narrowed shift for panning actions.\n   * @param  {Number} shift      Initial shift in pixels\n   * @param  {Number} minLimit   Minimum limit (left or top) in pixels\n   * @param  {Number} maxLimit   Maximum limit (right or bottom) in pixels\n   * @param  {Number} minElement Left or top element position in pixels\n   * @param  {Number} maxElement Right or bottom element position in pixels\n   * @return {Number}            Narrowed shift\n   */\n  const getLimitedShift = useCallback((\n    shift: number,\n    minLimit: number,\n    maxLimit: number,\n    minElement: number,\n    maxElement: number\n  ) => {\n    if (shift > 0) {\n      if (minElement > minLimit) {\n        // Forbid move if we are moving to left or top while we are already out minimum boudaries\n        return 0\n      } else if (minElement + shift > minLimit) {\n        // Lower the shift if we are going out boundaries\n        return minLimit - minElement\n      }\n    } else if (shift < 0) {\n      if (maxElement < maxLimit) {\n        // Forbid move if we are moving to right or bottom while we are already out maximum boudaries\n        return 0\n      } else if (maxElement + shift < maxLimit) {\n        // Lower the shift if we are going out boundaries\n        return maxLimit - maxElement\n      }\n    }\n\n    return shift\n  }, [])\n\n  const getCursor = useCallback((canMoveOnX: boolean, canMoveOnY: boolean) => {\n    if (canMoveOnX && canMoveOnY) {\n      return 'move'\n    } else if (canMoveOnX) {\n      return 'ew-resize'\n    } else if (canMoveOnY) {\n      return 'ns-resize'\n    } else {\n      return 'auto'\n    }\n  }, [])\n\n  /**\n   * Moves the element by incrementing its position with given X and Y values.\n   * @param  {Number} shiftX             Position change to apply on X axis in pixels\n   * @param  {Number} shiftY             Position change to apply on Y axis in pixels\n   * @param  {Number} transitionDuration Transition duration (in seconds)\n   */\n  const move = useCallback((shiftX: number, shiftY: number, transitionDuration = 0) => {\n    if (!ref.current) return\n    let newPosX = posRef.current[0]\n    let newPosY = posRef.current[1]\n\n    // Get container and container's parent coordinates\n    const rect = ref.current.getBoundingClientRect()\n    const parentRect = (ref.current.parentNode as HTMLElement).getBoundingClientRect()\n\n    const [isLarger, isOutLeftBoundary, isOutRightBoundary] = [\n      // Check if the element is larger than its container\n      rect.width > parentRect.right - parentRect.left,\n      // Check if the element is out its container left boundary\n      shiftX > 0 && rect.left - parentRect.left < 0,\n      // Check if the element is out its container right boundary\n      shiftX < 0 && rect.right - parentRect.right > 0,\n    ]\n\n    const canMoveOnX = isLarger || isOutLeftBoundary || isOutRightBoundary\n    if (canMoveOnX) {\n      newPosX += getLimitedShift(shiftX, parentRect.left, parentRect.right, rect.left, rect.right)\n    }\n\n    const [isHigher, isOutTopBoundary, isOutBottomBoundary] = [\n      // Check if the element is higher than its container\n      rect.height > parentRect.bottom - parentRect.top,\n      // Check if the element is out its container top boundary\n      shiftY > 0 && rect.top - parentRect.top < 0,\n      // Check if the element is out its container bottom boundary\n      shiftY < 0 && rect.bottom - parentRect.bottom > 0,\n    ]\n\n    const canMoveOnY = isHigher || isOutTopBoundary || isOutBottomBoundary\n    if (canMoveOnY) {\n      newPosY += getLimitedShift(shiftY, parentRect.top, parentRect.bottom, rect.top, rect.bottom)\n    }\n\n    const cursor = getCursor(canMoveOnX, canMoveOnY)\n\n    setPos([newPosX, newPosY])\n    setCursor(cursor)\n    setTransitionDuration(transitionDuration)\n  }, [setPos, setCursor, setTransitionDuration, getCursor, getLimitedShift])\n\n  /**\n   * Check if the user is doing a double tap gesture.\n   * @return {Boolean} Result of the checking\n   */\n  const isDoubleTapping = useCallback(() => {\n    const touchTime = new Date().getTime()\n    const isDoubleTap =\n      touchTime - (lastTouchTimeRef.current ?? 0) < doubleTouchMaxDelay &&\n      touchTime - (lastDoubleTapTimeRef.current ?? 0) > doubleTouchMaxDelay\n\n    if (isDoubleTap) {\n      lastDoubleTapTimeRef.current = touchTime\n      return true\n    }\n\n    lastTouchTimeRef.current = touchTime\n    return false\n  }, [doubleTouchMaxDelay])\n\n  /**\n   * Trigger a decelerating movement after a mouse up or a touch end event, using the last movement shift.\n   * @param  {Number} lastShiftOnX Last shift on the X axis in pixels\n   * @param  {Number} lastShiftOnY Last shift on the Y axis in pixels\n   */\n  const startDeceleration = useCallback((lastShiftOnX: number, lastShiftOnY: number) => {\n    let startTimestamp: number | null = null\n\n    const startDecelerationMove = (timestamp: number) => {\n      if (startTimestamp === null) startTimestamp = timestamp\n\n      const progress = timestamp - startTimestamp\n\n      // Calculates the ratio to apply on the move (used to create a non-linear deceleration)\n      const ratio = (decelerationDuration - progress) / decelerationDuration\n\n      const [shiftX, shiftY] = [lastShiftOnX * ratio, lastShiftOnY * ratio]\n\n      // Continue animation only if time has not expired and if there is still some movement (more than 1 pixel on one axis)\n      if (progress < decelerationDuration && Math.max(Math.abs(shiftX), Math.abs(shiftY)) > 1) {\n        move(shiftX, shiftY, 0)\n        lastRequestAnimationIdRef.current = requestAnimationFrame(startDecelerationMove)\n      } else {\n        lastRequestAnimationIdRef.current = null\n      }\n    }\n\n    lastRequestAnimationIdRef.current = requestAnimationFrame(startDecelerationMove)\n  }, [move, decelerationDuration])\n\n  /**\n   * Resets the component to its initial state.\n   */\n  const reset = useCallback(() => {\n    setZoom(initialZoom)\n    setCursor(defaultCursor)\n    setTransitionDuration(animDuration)\n    setPos(defaultPos)\n  }, [setZoom, setCursor, setTransitionDuration, setPos, initialZoom, animDuration]);\n\n  /**\n   * Event handler on double click.\n   * @param  {MouseEvent} event Mouse event\n   */\n  const handleDoubleClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {\n    event.preventDefault()\n    if (!allowZoom) return\n\n    if (zoomRef.current === minZoom) {\n      fullZoomInOnPosition(event.pageX, event.pageY)\n    } else {\n      reset()\n    }\n  }, [fullZoomInOnPosition, reset, allowZoom, minZoom])\n\n  /**\n   * Event handler on scroll.\n   * @param  {MouseEvent} event Mouse event\n   */\n  const handleMouseWheel = useCallback((event: WheelEvent) => {\n    event.preventDefault()\n    if (!allowZoom || !allowWheel) return\n\n    // Use the scroll event delta to determine the zoom velocity\n    const velocity = (-event.deltaY * scrollVelocity) / 100\n\n    // Set the new zoom level\n    const newZoom = Math.max(Math.min(zoomRef.current + velocity, maxZoom), minZoom)\n\n    let newPosition = posRef.current\n    if (newZoom !== zoomRef.current) {\n      newPosition = newZoom !== minZoom ? getNewPosition(event.pageX, event.pageY, newZoom) : defaultPos\n    }\n\n    setZoom(newZoom)\n    setPos(newPosition)\n    setTransitionDuration(0.05)\n  }, [getNewPosition, setZoom, setPos, setTransitionDuration, allowZoom, allowWheel, maxZoom, minZoom, scrollVelocity])\n\n  /**\n   * Event handler on mouse down.\n   * @param  {MouseEvent} event Mouse event\n   */\n  const handleMouseStart = useCallback((event: MouseEvent) => {\n    event.preventDefault()\n    if (!allowPan || ignoredMouseButtons.includes(event.button)) return\n\n    if (lastRequestAnimationIdRef.current) cancelAnimationFrame(lastRequestAnimationIdRef.current)\n    lastCursorRef.current = [event.pageX, event.pageY]\n  }, [allowPan, ignoredMouseButtons]);\n\n  /**\n   * Event handler on mouse move.\n   * @param  {MouseEvent} event Mouse event\n   */\n  const handleMouseMove = useCallback((event: MouseEvent) => {\n    event.preventDefault()\n\n    if (!allowPan || !lastCursorRef.current) return\n\n    const [posX, posY] = [event.pageX, event.pageY]\n    const shiftX = posX - lastCursorRef.current[0]\n    const shiftY = posY - lastCursorRef.current[1]\n\n    move(shiftX, shiftY, 0)\n\n    lastCursorRef.current = [posX, posY]\n    lastShiftRef.current = [shiftX, shiftY]\n  }, [move, allowPan]);\n\n  /**\n   * Event handler on mouse up or mouse out.\n   * @param  {MouseEvent} event Mouse event\n   */\n  const handleMouseStop = useCallback((event: MouseEvent) => {\n    event.preventDefault()\n\n    if (lastShiftRef.current) {\n      // Use the last shift to make a decelerating movement effect\n      startDeceleration(lastShiftRef.current[0], lastShiftRef.current[1])\n      lastShiftRef.current = null\n    }\n\n    lastCursorRef.current = null\n    setCursor('auto')\n  }, [startDeceleration])\n\n  /**\n   * Event handler on touch start.\n   * Zoom-in at the maximum scale if a double tap is detected.\n   * @param  {TouchEvent} event Touch event\n   */\n  const handleTouchStart = useCallback((event: TouchEvent) => {\n    const isThisDoubleTapping = isDoubleTapping()\n    const isMultiTouch = event.touches.length > 1\n\n    if (!allowTouchEvents) event.preventDefault()\n\n    if (lastRequestAnimationIdRef.current) cancelAnimationFrame(lastRequestAnimationIdRef.current)\n\n    const [posX, posY] = [event.touches[0].pageX, event.touches[0].pageY]\n\n    if (isMultiTouch) {\n      lastTouchRef.current = [posX, posY]\n      return\n    }\n\n    if (isThisDoubleTapping && allowZoom) {\n      if (zoomRef.current === minZoom) {\n        fullZoomInOnPosition(posX, posY)\n      } else {\n        reset()\n      }\n\n      return\n    }\n\n    // Don't save the last touch if we are starting a simple touch move while panning is disabled\n    if (allowPan) lastTouchRef.current = [posX, posY]\n  }, [\n    fullZoomInOnPosition,\n    reset,\n    isDoubleTapping,\n    allowZoom,\n    allowTouchEvents,\n    allowPan,\n    minZoom\n  ])\n\n  /**\n   * Event handler on touch move.\n   * Either move the element using one finger or zoom-in with a two finger pinch.\n   * @param  {TouchEvent} event Touch move\n   */\n  const handleTouchMove = useCallback((event: TouchEvent) => {\n    if (!allowTouchEvents) event.preventDefault()\n    if (!lastTouchRef.current) return\n\n    if (event.touches.length === 1) {\n      const [posX, posY] = [event.touches[0].pageX, event.touches[0].pageY]\n      // If we detect only one point, we shall just move the element\n      const shiftX = posX - lastTouchRef.current[0]\n      const shiftY = posY - lastTouchRef.current[1]\n\n      move(shiftX, shiftY)\n      lastShiftRef.current = [shiftX, shiftY]\n\n      // Save data for the next move\n      lastTouchRef.current = [posX, posY]\n      lastTouchDistanceRef.current = null\n    } else if (event.touches.length > 1) {\n      let newZoom = zoomRef.current\n      // If we detect two points, we shall zoom up or down\n      const [pos1X, pos1Y] = [event.touches[0].pageX, event.touches[0].pageY]\n      const [pos2X, pos2Y] = [event.touches[1].pageX, event.touches[1].pageY]\n      const distance = Math.sqrt(Math.pow(pos2X - pos1X, 2) + Math.pow(pos2Y - pos1Y, 2))\n\n      if (lastTouchDistanceRef.current && distance && distance !== lastTouchDistanceRef.current) {\n        if (allowZoom) {\n          newZoom += (distance - lastTouchDistanceRef.current) / 100\n          if (newZoom > maxZoom) {\n            newZoom = maxZoom\n          } else if (newZoom < minZoom) {\n            newZoom = minZoom\n          }\n        }\n\n        // Change position using the center point between the two fingers\n        const [centerX, centerY] = [(pos1X + pos2X) / 2, (pos1Y + pos2Y) / 2]\n        const newPos = getNewPosition(centerX, centerY, newZoom)\n\n        setZoom(newZoom)\n        setPos(newPos)\n        setTransitionDuration(0)\n      }\n\n      // Save data for the next move\n      lastTouchRef.current = [pos1X, pos1Y]\n      lastTouchDistanceRef.current = distance\n    }\n  }, [\n    getNewPosition,\n    move,\n    setPos,\n    setTransitionDuration,\n    setZoom,\n    allowZoom,\n    allowTouchEvents,\n    maxZoom,\n    minZoom\n  ])\n\n  /**\n   * Event handler on touch end or touch cancel.\n   * @param  {TouchEvent} event Touch move\n   */\n  const handleTouchStop = useCallback(() => {\n    if (lastShiftRef.current) {\n      // Use the last shift to make a decelerating movement effect\n      startDeceleration(lastShiftRef.current[0], lastShiftRef.current[1])\n      lastShiftRef.current = null\n    }\n\n    lastTouchRef.current = null\n    lastTouchDistanceRef.current = null\n  }, [startDeceleration])\n\n  // Imperative Ref methods\n  useImperativeHandle(forwardedRef, () => ({\n    getZoom,\n    zoomIn,\n    reset,\n    move,\n    zoomOut,\n    zoomToZone,\n    setZoom,\n    setPos,\n  }))\n\n  useEffect(() => {\n    const refCurrentValue = ref.current\n    const hasMouseDevice = window.matchMedia('(pointer: fine)').matches\n\n    refCurrentValue?.addEventListener('wheel', handleMouseWheel, { passive: false })\n    if (hasMouseDevice) {\n      // Apply mouse events only to devices which include an accurate pointing device\n      refCurrentValue?.addEventListener('mousedown', handleMouseStart, { passive: false })\n      refCurrentValue?.addEventListener('mousemove', handleMouseMove, { passive: false })\n      refCurrentValue?.addEventListener('mouseup', handleMouseStop, { passive: false })\n      refCurrentValue?.addEventListener('mouseleave', handleMouseStop, { passive: false })\n    } else {\n      // Apply touch events to all other devices\n      refCurrentValue?.addEventListener('touchstart', handleTouchStart, { passive: false })\n      refCurrentValue?.addEventListener('touchmove', handleTouchMove, { passive: false })\n      refCurrentValue?.addEventListener('touchend', handleTouchStop, { passive: false })\n      refCurrentValue?.addEventListener('touchcancel', handleTouchStop, { passive: false })\n    }\n\n    return () => {\n      refCurrentValue?.removeEventListener('wheel', handleMouseWheel)\n      if (hasMouseDevice) {\n        refCurrentValue?.removeEventListener('mousedown', handleMouseStart)\n        refCurrentValue?.removeEventListener('mousemove', handleMouseMove)\n        refCurrentValue?.removeEventListener('mouseup', handleMouseStop)\n        refCurrentValue?.removeEventListener('mouseleave', handleMouseStop)\n      } else {\n        refCurrentValue?.removeEventListener('touchstart', handleTouchStart)\n        refCurrentValue?.removeEventListener('touchmove', handleTouchMove)\n        refCurrentValue?.removeEventListener('touchend', handleTouchStop)\n        refCurrentValue?.removeEventListener('touchcancel', handleTouchStop)\n      }\n    }\n  }, [\n    handleMouseWheel,\n    handleMouseStart,\n    handleMouseMove,\n    handleMouseStop,\n    handleTouchStart,\n    handleTouchMove,\n    handleTouchStop\n  ])\n\n  const attr = {\n    ...divProps,\n    ref,\n    onDoubleClick: handleDoubleClick,\n    style: {\n      ...divProps.style,\n      cursor: cursor,\n      willChange: 'transform',\n      transition: `transform ease-out ${transitionRef.current}s`,\n      touchAction: allowParentPanning && zoomRef.current === 1 ? 'pan-x pan-y' : 'none',\n      transform: `translate3d(${posRef.current[0]}px, ${posRef.current[1]}px, 0) scale(${zoomRef.current})`,\n    },\n  }\n\n  return <div {...attr}>{children}</div>\n})\n\nexport default PrismaZoom\n"
  },
  {
    "path": "src/types.ts",
    "content": "export type Ref = {\n  getZoom: () => number\n  zoomIn: (zoom: number) => void\n  zoomOut: (zoom: number) => void\n  move: (shiftX: number, shiftY: number, transitionDuration?: number) => void\n  reset: VoidFunction\n  zoomToZone: (relX: number, relY: number, relWidth: number, relHeight: number) => void\n}\n\nexport type Props = NonNullable<React.PropsWithChildren> &\n  React.HTMLAttributes<HTMLDivElement> & {\n    /**  Minimum zoom ratio */\n    minZoom?: number\n    /**\n     * Maximum zoom ratio\n     */\n    maxZoom?: number\n    /**\n     * Initial zoom ratio\n     */\n    initialZoom?: number\n    /**\n     * Zoom increment or decrement on each scroll wheel detection\n     */\n    scrollVelocity?: number\n    /**\n     * Function called each time the zoom value changes\n     */\n    onZoomChange?: (zoom: number) => void\n    /**\n     * Function called each time the posX or posY value changes (aka images was panned)\n     */\n    onPanChange?: (props: { posX: number; posY: number }) => void\n    /**\n     * Animation duration (in seconds)\n     */\n    animDuration?: number\n    /**\n     * Max delay between two taps to consider a double tap (in milliseconds)\n     */\n    doubleTouchMaxDelay?: number\n    /**\n     * Decelerating movement duration after a mouse up or a touch end event (in milliseconds)\n     */\n    decelerationDuration?: number\n    /**\n     * Enable or disable zooming in place\n     */\n    allowZoom?: boolean\n    /**\n     * Enable or disable panning in place\n     */\n    allowPan?: boolean\n    /**\n     * By default, all touch events are caught (if set to true touch events propagate)\n     */\n    allowTouchEvents?: boolean\n    /**\n     * By default, page cannot scroll with touch events\n     */\n    allowParentPanning?: boolean\n    /**\n     * Enable or disable mouse wheel and touchpad zooming in place\n     */\n    allowWheel?: boolean\n    /**\n     * Optional array of ignored mouse buttons allows to prevent panning for specific mouse buttons. By default all mouse buttons are enabled\n     * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button#value\n     */\n    ignoredMouseButtons?: number[]\n  }\n\nexport type PositionType = [number, number]\n\nexport type CursorType = React.CSSProperties['cursor']\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"include\": [\"src/index.tsx\", \"src/types.ts\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist/esm\",\n    \"module\": \"esnext\",\n    \"target\": \"es5\",\n    \"lib\": [\"es6\", \"dom\", \"esnext\"],\n    \"jsx\": \"react-jsx\",\n    \"declaration\": true,\n    \"moduleResolution\": \"node\",\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"esModuleInterop\": true,\n    \"noImplicitReturns\": true,\n    \"noImplicitThis\": true,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true\n  }\n}\n"
  }
]