[
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\n\nexample\nindex.html\nvite.config.mts\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist\n"
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  singleQuote: true,\n  plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],\n};\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 nanxiaobei\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.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\nLink in bio to **widgets**,\nyour online **home screen**. ➫ [🔗 kee.so](https://kee.so/)\n\n</div>\n\n---\n\n# antd-img-crop\n\nAn image cropper for Ant Design [Upload](https://ant.design/components/upload/)\n\n[![npm](https://img.shields.io/npm/v/antd-img-crop.svg?style=flat-square)](https://www.npmjs.com/package/antd-img-crop)\n[![npm](https://img.shields.io/npm/dt/antd-img-crop?style=flat-square)](https://www.npmtrends.com/antd-img-crop)\n[![npm bundle size](https://img.shields.io/bundlephobia/minzip/antd-img-crop?style=flat-square)](https://bundlephobia.com/result?p=antd-img-crop)\n[![GitHub](https://img.shields.io/github/license/nanxiaobei/antd-img-crop?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE)\n[![npm type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/src/types.ts)\n\nEnglish | [简体中文](./README.zh-CN.md)\n\n## Demo\n\n[![Edit antd-img-crop](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/antd-img-crop-5x4j3r)\n\n## Install\n\n```sh\npnpm add antd-img-crop\n# or\nyarn add antd-img-crop\n# or\nnpm i antd-img-crop\n```\n\n## Usage\n\n```jsx harmony\nimport { Upload } from 'antd';\nimport ImgCrop from 'antd-img-crop';\n\nconst Demo = () => (\n  <ImgCrop>\n    <Upload>+ Add image</Upload>\n  </ImgCrop>\n);\n```\n\n## Props\n\n| Prop           | Type       | Default        | Description                                                                      |\n| -------------- | ---------- | -------------- | -------------------------------------------------------------------------------- |\n| quality        | `number`   | `0.4`          | Cropped image quality, `0` to `1`                                                |\n| fillColor      | `string`   | `'white'`      | Fill color for cropped image                                                     |\n| zoomSlider     | `boolean`  | `true`         | Enable zoom                                                                      |\n| rotationSlider | `boolean`  | `false`        | Enable rotation                                                                  |\n| aspectSlider   | `boolean`  | `false`        | Enable aspect                                                                    |\n| showReset      | `boolean`  | `false`        | Show reset button to reset zoom & rotation & aspect                              |\n| resetText      | `string`   | `Reset`        | Reset button text                                                                |\n| aspect         | `number`   | `1 / 1`        | Aspect of crop area , `width / height`                                           |\n| minZoom        | `number`   | `1`            | Minimum zoom                                                                     |\n| maxZoom        | `number`   | `3`            | Maximum zoom                                                                     |\n| minAspect      | `number`   | `0.5`          | Minimum aspect                                                                   |\n| maxAspect      | `number`   | `2`            | Maximum aspect                                                                   |\n| cropShape      | `string`   | `'rect'`       | Shape of crop area, `'rect'` or `'round'`                                        |\n| showGrid       | `boolean`  | `false`        | Show grid of crop area (third-lines)                                             |\n| cropperProps   | `object`   | -              | [react-easy-crop] props (\\* existing props cannot be overridden)                 |\n| modalClassName | `string`   | `''`           | Modal classname                                                                  |\n| modalTitle     | `string`   | `'Edit image'` | Modal title                                                                      |\n| modalWidth     | `number`   | `string`       | Modal width                                                                      |\n| modalOk        | `string`   |                | Ok button text                                                                   |\n| modalCancel    | `string`   |                | Cancel button text                                                               |\n| onModalOk      | `function` | -              | Callback of click ok button                                                      |\n| onModalCancel  | `function` | -              | Callback of click cancel button (or modal mask & top right \"x\")                  |\n| modalProps     | `object`   |                | [Ant Design Modal] props (\\* existing props cannot be overridden)                |\n| beforeCrop     | `function` | -              | Callback before crop modal, if return `false` or `reject()`, modal will not open |\n\n## FAQ\n\n### `ConfigProvider` not work?\n\nTry to set `libraryDirectory`(`'es'` or `'lib'`) to `babel-plugin-import` config, see which one will work.\n\n```js\nmodule.exports = {\n  plugins: [\n    ['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }],\n  ],\n};\n```\n\n### No style? (only `antd<=v4`)\n\nIf you use `antd<=v4` + `babel-plugin-import`, and no `Modal` or `Slider` were imported, please import these styles manually:\n\n```js\nimport 'antd/es/modal/style';\nimport 'antd/es/slider/style';\n```\n\n## License\n\n[MIT License](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE) (c) [nanxiaobei](https://lee.so/)\n\n[react-easy-crop]: https://github.com/ValentinH/react-easy-crop#props\n[Ant Design Modal]: https://ant.design/components/modal#api\n"
  },
  {
    "path": "README.zh-CN.md",
    "content": "<div align=\"center\">\n\nLink in bio to **widgets**,\nyour online **home screen**. ➫ [🔗 kee.so](https://kee.so/)\n\n</div>\n\n---\n\n# antd-img-crop\n\n图片裁切工具，用于 Ant Design [Upload](https://ant.design/components/upload-cn/) 组件\n\n[![npm](https://img.shields.io/npm/v/antd-img-crop.svg?style=flat-square)](https://www.npmjs.com/package/antd-img-crop)\n[![npm](https://img.shields.io/npm/dt/antd-img-crop?style=flat-square)](https://www.npmtrends.com/antd-img-crop)\n[![npm bundle size](https://img.shields.io/bundlephobia/minzip/antd-img-crop?style=flat-square)](https://bundlephobia.com/result?p=antd-img-crop)\n[![GitHub](https://img.shields.io/github/license/nanxiaobei/antd-img-crop?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE)\n[![npm type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/src/types.ts)\n\n[English](./README.md) | 简体中文\n\n## 示例\n\n[![Edit antd-img-crop](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/antd-img-crop-5x4j3r)\n\n## 安装\n\n```sh\npnpm add antd-img-crop\n# or\nyarn add antd-img-crop\n# or\nnpm i antd-img-crop\n```\n\n## 使用\n\n```jsx harmony\nimport { Upload } from 'antd';\nimport ImgCrop from 'antd-img-crop';\n\nconst Demo = () => (\n  <ImgCrop>\n    <Upload>+ Add image</Upload>\n  </ImgCrop>\n);\n```\n\n## Props\n\n| 属性           | 类型                 | 默认         | 说明                                                             |\n| -------------- | -------------------- | ------------ | ---------------------------------------------------------------- |\n| quality        | `number`             | `0.4`        | 裁切图片质量，`0` 到 `1` 之间                                    |\n| fillColor      | `string`             | `'white'`    | 裁切图像填充色                                                   |\n| zoomSlider     | `boolean`            | `true`       | 允许缩放                                                         |\n| rotationSlider | `boolean`            | `false`      | 允许旋转                                                         |\n| aspectSlider   | `boolean`            | `false`      | 允许调整裁切比                                                   |\n| showReset      | `boolean`            | `false`      | 显示重置按钮，重置缩放 & 旋转 & 裁切比                           |\n| resetText      | `string`             | `重置`       | 重置按钮文字                                                     |\n| aspect         | `number`             | `1 / 1`      | 裁切区域宽高比，`width / height`                                 |\n| minZoom        | `number`             | `1`          | 最小缩放                                                         |\n| maxZoom        | `number`             | `3`          | 最大缩放                                                         |\n| minAspect      | `number`             | `0.5`        | 最小裁切比                                                       |\n| maxAspect      | `number`             | `2`          | 最大裁切比                                                       |\n| cropShape      | `string`             | `'rect'`     | 裁切区域形状，`'rect'` 或 `'round'`                              |\n| showGrid       | `boolean`            | `false`      | 显示裁切区域网格（九宫格）                                       |\n| cropperProps   | `object`             | -            | [react-easy-crop] 的 props（\\* 已有 props 无法重写）             |\n| modalClassName | `string`             | `''`         | 弹窗 className                                                   |\n| modalTitle     | `string`             | `'编辑图片'` | 弹窗标题                                                         |\n| modalWidth     | `number` \\| `string` |              | 弹窗宽度                                                         |\n| modalOk        | `string`             |              | 确定按钮文字                                                     |\n| modalCancel    | `string`             |              | 取消按钮文字                                                     |\n| onModalOK      | `function`           | -            | 点击确定按钮的回调                                               |\n| onModalCancel  | `function`           | -            | 点击取消按钮、遮罩层、或右上角 'x' 的回调                        |\n| modalProps     | `object`             |              | [Ant Design Modal] 的 props（\\* 已有 props 无法重写）            |\n| beforeCrop     | `function`           | -            | 裁切弹窗打开前的回调，若返回 `false` 或 `reject`，弹窗将不会打开 |\n\n## FAQ\n\n### `ConfigProvider` 无效？\n\n尝试设置 `libraryDirectory`（`'es'` 或 `'lib'`）到 `babel-plugin-import` 的配置项，看看哪个会生效。\n\n```js\nmodule.exports = {\n  plugins: [\n    ['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }],\n  ],\n};\n```\n\n### 没有样式？（仅 `antd<=v4`）\n\n若使用 `antd<=v4` + `babel-plugin-import`，且未引入 `Modal` 或 `Slider`，请手动引入这些样式：\n\n```js\nimport 'antd/es/modal/style';\nimport 'antd/es/slider/style';\n```\n\n## 协议\n\n[MIT License](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE) (c) [nanxiaobei](https://lee.so/)\n\n[react-easy-crop]: https://github.com/ValentinH/react-easy-crop#props\n[Ant Design Modal]: https://ant.design/components/modal-cn#api\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import { fixupConfigRules } from '@eslint/compat';\nimport { FlatCompat } from '@eslint/eslintrc';\nimport js from '@eslint/js';\nimport { defineConfig, globalIgnores } from 'eslint/config';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n  recommendedConfig: js.configs.recommended,\n  allConfig: js.configs.all,\n});\n\nexport default defineConfig([\n  globalIgnores(['**/dist']),\n  {\n    extends: fixupConfigRules(compat.extends('react-app', 'prettier')),\n  },\n]);\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"antd-img-crop\",\n  \"version\": \"4.30.0\",\n  \"description\": \"An image cropper for Ant Design Upload\",\n  \"keywords\": [\n    \"react\",\n    \"antd\",\n    \"ant-design\",\n    \"upload\",\n    \"image\",\n    \"crop\",\n    \"cropper\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"nanxiaobei <nanxiaobei@gmail.com> (https://github.com/nanxiaobei)\",\n  \"homepage\": \"https://github.com/nanxiaobei/antd-img-crop\",\n  \"repository\": \"github:nanxiaobei/antd-img-crop\",\n  \"bugs\": \"https://github.com/nanxiaobei/antd-img-crop/issues\",\n  \"types\": \"./dist/antd-img-crop.d.ts\",\n  \"module\": \"./dist/antd-img-crop.esm.js\",\n  \"main\": \"./dist/antd-img-crop.cjs.js\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/antd-img-crop.d.ts\",\n      \"import\": \"./dist/antd-img-crop.esm.js\",\n      \"require\": \"./dist/antd-img-crop.cjs.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"rm -rf dist && rollup -c --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs\"\n  },\n  \"peerDependencies\": {\n    \"antd\": \">=4.0.0\",\n    \"react\": \">=16.8.0\",\n    \"react-dom\": \">=16.8.0\"\n  },\n  \"dependencies\": {\n    \"react-easy-crop\": \"^5.5.6\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"@eslint/compat\": \"^2.0.3\",\n    \"@eslint/eslintrc\": \"^3.3.5\",\n    \"@eslint/js\": \"^10.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^16.0.3\",\n    \"@rollup/plugin-replace\": \"^6.0.3\",\n    \"@rollup/plugin-typescript\": \"^12.3.0\",\n    \"@tailwindcss/postcss\": \"^4.2.1\",\n    \"@types/node\": \"^25.5.0\",\n    \"@types/react\": \"19.2.14\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.57.0\",\n    \"@vitejs/plugin-react\": \"^6.0.0\",\n    \"antd\": \"^6.3.2\",\n    \"autoprefixer\": \"^10.4.27\",\n    \"eslint\": \"^10.0.3\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-config-react-app\": \"^7.0.1\",\n    \"eslint-plugin-flowtype\": \"^8.0.3\",\n    \"eslint-plugin-import\": \"^2.32.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"postcss\": \"^8.5.8\",\n    \"prettier\": \"^3.8.1\",\n    \"prettier-plugin-organize-imports\": \"^4.3.0\",\n    \"prettier-plugin-tailwindcss\": \"^0.7.2\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"rollup\": \"4.59.0\",\n    \"rollup-plugin-dts\": \"^6.4.0\",\n    \"rollup-plugin-postcss\": \"^4.0.2\",\n    \"tailwindcss\": \"^4.2.1\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^8.0.0\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n};\n"
  },
  {
    "path": "rollup.config.ts",
    "content": "import nodeResolve from '@rollup/plugin-node-resolve';\nimport replace from '@rollup/plugin-replace';\nimport typescript from '@rollup/plugin-typescript';\nimport type { RollupOptions } from 'rollup';\nimport dts from 'rollup-plugin-dts';\nimport postcss from 'rollup-plugin-postcss';\nimport pkg from './package.json';\n\nconst input = 'src/ImgCrop.tsx';\n\nconst cjsOutput = { file: pkg.main, format: 'cjs', exports: 'auto' } as const;\nconst esmOutput = { file: pkg.module, format: 'es' } as const;\nconst dtsOutput = { file: pkg.types, format: 'es' } as const;\n\nconst nodePlugin = nodeResolve();\nconst tsPlugin = typescript({ compilerOptions: { module: 'esnext' } });\nconst postcssPlugin = postcss({\n  minimize: true,\n  inject: (cssVariableName) => `\n    (function() {\n      if (typeof document === 'undefined') return;\n      const style = document.createElement('style');\n      const meta = document.querySelector('meta[name=\"csp-nonce\"]');\n      if (meta && meta.content) style.setAttribute('nonce', meta.content);\n      style.textContent = ${cssVariableName};\n      document.head.appendChild(style);\n    })();\n    `,\n});\nconst replacePlugin = replace({ preventAssignment: true, '/es/': '/lib/' });\n\nconst cjsPlugins = [nodePlugin, tsPlugin, postcssPlugin, replacePlugin];\nconst esmPlugins = [nodePlugin, tsPlugin, postcssPlugin];\n\nconst external = [\n  ...Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies }),\n  /^react($|\\/)/,\n  /^antd($|\\/)/,\n];\n\nconst config: RollupOptions[] = [\n  { input, output: cjsOutput, plugins: cjsPlugins, external },\n  { input, output: esmOutput, plugins: esmPlugins, external },\n  { input, output: dtsOutput, plugins: [dts()], external: [/\\.css$/] },\n];\n\nexport default config;\n"
  },
  {
    "path": "src/EasyCrop.tsx",
    "content": "import AntButton from 'antd/es/button';\nimport AntSlider from 'antd/es/slider';\nimport {\n  forwardRef,\n  memo,\n  useCallback,\n  useImperativeHandle,\n  useRef,\n  useState,\n} from 'react';\nimport Cropper from 'react-easy-crop';\nimport type { Area, Point } from 'react-easy-crop';\nimport {\n  ASPECT_STEP,\n  PREFIX,\n  ROTATION_INITIAL,\n  ROTATION_MAX,\n  ROTATION_MIN,\n  ROTATION_STEP,\n  ZOOM_INITIAL,\n  ZOOM_STEP,\n} from './constants';\nimport type { EasyCropProps, EasyCropRef } from './types';\n\nconst EasyCrop = forwardRef<EasyCropRef, EasyCropProps>((props, ref) => {\n  const {\n    cropperRef,\n    zoomSlider,\n    rotationSlider,\n    aspectSlider,\n    showReset,\n    resetBtnText,\n\n    modalImage,\n    aspect: propAspect,\n    minZoom,\n    maxZoom,\n    minAspect,\n    maxAspect,\n    cropShape,\n    showGrid,\n\n    cropperProps,\n  } = props;\n\n  const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });\n  const [zoom, setZoom] = useState(ZOOM_INITIAL);\n  const [rotation, setRotation] = useState(ROTATION_INITIAL);\n  const [aspect, setAspect] = useState(propAspect);\n\n  const cropPixelsRef = useRef<Area>({ width: 0, height: 0, x: 0, y: 0 });\n  const onCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => {\n    cropPixelsRef.current = croppedAreaPixels;\n  }, []);\n\n  const prevPropAspect = useRef(propAspect);\n  if (prevPropAspect.current !== propAspect) {\n    prevPropAspect.current = propAspect;\n    setAspect(propAspect);\n  }\n\n  const isResetActive =\n    zoom !== ZOOM_INITIAL ||\n    rotation !== ROTATION_INITIAL ||\n    aspect !== propAspect;\n\n  const onReset = () => {\n    setZoom(ZOOM_INITIAL);\n    setRotation(ROTATION_INITIAL);\n    setAspect(propAspect);\n  };\n\n  useImperativeHandle(ref, () => ({\n    rotation,\n    cropPixelsRef,\n    onReset,\n  }));\n\n  const wrapperClass =\n    '[display:flex] [align-items:center] [width:60%] [margin-inline:auto]';\n\n  const buttonClass =\n    '[display:flex] [align-items:center] [justify-content:center] [height:32px] [width:32px] [background:transparent] [border:0] [font-family:inherit] [font-size:18px] [cursor:pointer] disabled:[opacity:20%] disabled:[cursor:default]';\n\n  const sliderClass = '[flex:1]';\n\n  return (\n    <>\n      <Cropper\n        {...cropperProps}\n        ref={cropperRef}\n        image={modalImage}\n        crop={crop}\n        zoom={zoom}\n        rotation={rotation}\n        aspect={aspect}\n        minZoom={minZoom}\n        maxZoom={maxZoom}\n        zoomWithScroll={zoomSlider}\n        cropShape={cropShape}\n        showGrid={showGrid}\n        onCropChange={setCrop}\n        onZoomChange={setZoom}\n        onRotationChange={setRotation}\n        onCropComplete={onCropComplete}\n        classes={{\n          containerClassName: `${PREFIX}-container ![position:relative] [width:100%] [height:40vh] [&~section:first-of-type]:[margin-top:16px] [&~section:last-of-type]:[margin-bottom:16px]`,\n          mediaClassName: `${PREFIX}-media`,\n        }}\n      />\n\n      {zoomSlider && (\n        <section\n          className={`${PREFIX}-control ${PREFIX}-control-zoom ${wrapperClass}`}\n        >\n          <button\n            className={buttonClass}\n            onClick={() => setZoom(+(zoom - ZOOM_STEP).toFixed(1))}\n            disabled={zoom - ZOOM_STEP < minZoom}\n          >\n            －\n          </button>\n          <AntSlider\n            className={sliderClass}\n            min={minZoom}\n            max={maxZoom}\n            step={ZOOM_STEP}\n            value={zoom}\n            onChange={setZoom}\n          />\n          <button\n            className={buttonClass}\n            onClick={() => setZoom(+(zoom + ZOOM_STEP).toFixed(1))}\n            disabled={zoom + ZOOM_STEP > maxZoom}\n          >\n            ＋\n          </button>\n        </section>\n      )}\n\n      {rotationSlider && (\n        <section\n          className={`${PREFIX}-control ${PREFIX}-control-rotation ${wrapperClass}`}\n        >\n          <button\n            className={`${buttonClass} [font-size:16px]`}\n            onClick={() => setRotation(rotation - ROTATION_STEP)}\n            disabled={rotation === ROTATION_MIN}\n          >\n            ↺\n          </button>\n          <AntSlider\n            className={sliderClass}\n            min={ROTATION_MIN}\n            max={ROTATION_MAX}\n            step={ROTATION_STEP}\n            value={rotation}\n            onChange={setRotation}\n          />\n          <button\n            className={`${buttonClass} [font-size:16px]`}\n            onClick={() => setRotation(rotation + ROTATION_STEP)}\n            disabled={rotation === ROTATION_MAX}\n          >\n            ↻\n          </button>\n        </section>\n      )}\n\n      {aspectSlider && (\n        <section\n          className={`${PREFIX}-control ${PREFIX}-control-aspect ${wrapperClass}`}\n        >\n          <button\n            className={buttonClass}\n            onClick={() => setAspect(+(aspect - ASPECT_STEP).toFixed(2))}\n            disabled={aspect - ASPECT_STEP < minAspect}\n          >\n            ↕\n          </button>\n          <AntSlider\n            className={sliderClass}\n            min={minAspect}\n            max={maxAspect}\n            step={ASPECT_STEP}\n            value={aspect}\n            onChange={setAspect}\n          />\n          <button\n            className={buttonClass}\n            onClick={() => setAspect(+(aspect + ASPECT_STEP).toFixed(2))}\n            disabled={aspect + ASPECT_STEP > maxAspect}\n          >\n            ↔\n          </button>\n        </section>\n      )}\n\n      {showReset && (zoomSlider || rotationSlider || aspectSlider) && (\n        <AntButton\n          className=\"[position:absolute] [bottom:20px]\"\n          style={isResetActive ? {} : { opacity: 0.3, pointerEvents: 'none' }}\n          onClick={onReset}\n        >\n          {resetBtnText}\n        </AntButton>\n      )}\n    </>\n  );\n});\n\nexport default memo(EasyCrop);\n"
  },
  {
    "path": "src/ImgCrop.css",
    "content": "@tailwind utilities;\n"
  },
  {
    "path": "src/ImgCrop.tsx",
    "content": "import type { ModalProps } from 'antd';\nimport { version } from 'antd';\nimport AntModal from 'antd/es/modal';\nimport AntUpload from 'antd/es/upload';\nimport type { RcFile, UploadFile } from 'antd/es/upload/interface';\nimport type { MouseEvent, ReactNode } from 'react';\nimport { forwardRef, useCallback, useMemo, useRef, useState } from 'react';\nimport type CropperRef from 'react-easy-crop';\nimport EasyCrop from './EasyCrop';\nimport './ImgCrop.css';\nimport { PREFIX, ROTATION_INITIAL } from './constants';\nimport type {\n  BeforeUpload,\n  BeforeUploadReturnType,\n  EasyCropRef,\n  ImgCropProps,\n} from './types';\n\n// v1 >= v2\nconst isGeThan = (v1: string, v2: string): boolean => {\n  const arr1 = v1.split('.').map(Number);\n  const arr2 = v2.split('.').map(Number);\n  const len = Math.max(arr1.length, arr2.length);\n  for (let i = 0; i < len; i++) {\n    const a = arr1[i] ?? 0;\n    const b = arr2[i] ?? 0;\n    if (a > b) return true;\n    if (a < b) return false;\n  }\n  return true;\n};\n\nexport type { ImgCropProps } from './types';\n\nconst ImgCrop = forwardRef<CropperRef, ImgCropProps>((props, cropperRef) => {\n  const {\n    quality = 0.4,\n    fillColor = 'white',\n\n    zoomSlider = true,\n    rotationSlider = false,\n    aspectSlider = false,\n    showReset = false,\n    resetText,\n\n    aspect = 1,\n    minZoom = 1,\n    maxZoom = 3,\n    minAspect = 0.5,\n    maxAspect = 2,\n    cropShape = 'rect',\n    showGrid = false,\n    cropperProps,\n\n    modalClassName,\n    modalTitle,\n    modalWidth,\n    modalOk,\n    modalCancel,\n    onModalOk,\n    onModalCancel,\n    modalProps,\n\n    beforeCrop,\n    children,\n  } = props;\n\n  const cb = useRef<\n    Pick<ImgCropProps, 'onModalOk' | 'onModalCancel' | 'beforeCrop'>\n  >({});\n  cb.current.onModalOk = onModalOk;\n  cb.current.onModalCancel = onModalCancel;\n  cb.current.beforeCrop = beforeCrop;\n\n  /**\n   * crop\n   */\n  const easyCropRef = useRef<EasyCropRef>(null);\n  const getCropCanvas = useCallback(\n    (target: ShadowRoot) => {\n      const canvas = document.createElement('canvas');\n      const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;\n      const context = (target?.getRootNode?.() as ShadowRoot) || document;\n\n      type ImgSource = CanvasImageSource & {\n        naturalWidth: number;\n        naturalHeight: number;\n      };\n\n      const imgSource = context.querySelector(`.${PREFIX}-media`) as ImgSource;\n\n      const {\n        width: cropWidth,\n        height: cropHeight,\n        x: cropX,\n        y: cropY,\n      } = easyCropRef.current!.cropPixelsRef.current;\n\n      if (\n        rotationSlider &&\n        easyCropRef.current!.rotation !== ROTATION_INITIAL\n      ) {\n        const { naturalWidth: imgWidth, naturalHeight: imgHeight } = imgSource;\n        const angle = easyCropRef.current!.rotation * (Math.PI / 180);\n\n        // get container for rotated image\n        const sine = Math.abs(Math.sin(angle));\n        const cosine = Math.abs(Math.cos(angle));\n        const squareWidth = imgWidth * cosine + imgHeight * sine;\n        const squareHeight = imgHeight * cosine + imgWidth * sine;\n\n        canvas.width = squareWidth;\n        canvas.height = squareHeight;\n        ctx.fillStyle = fillColor;\n        ctx.fillRect(0, 0, squareWidth, squareHeight);\n\n        // rotate container\n        const squareHalfWidth = squareWidth / 2;\n        const squareHalfHeight = squareHeight / 2;\n        ctx.translate(squareHalfWidth, squareHalfHeight);\n        ctx.rotate(angle);\n        ctx.translate(-squareHalfWidth, -squareHalfHeight);\n\n        // draw rotated image\n        const imgX = (squareWidth - imgWidth) / 2;\n        const imgY = (squareHeight - imgHeight) / 2;\n        ctx.drawImage(\n          imgSource,\n          0,\n          0,\n          imgWidth,\n          imgHeight,\n          imgX,\n          imgY,\n          imgWidth,\n          imgHeight,\n        );\n\n        // crop rotated image\n        const imgData = ctx.getImageData(0, 0, squareWidth, squareHeight);\n        canvas.width = cropWidth;\n        canvas.height = cropHeight;\n        ctx.putImageData(imgData, -cropX, -cropY);\n      } else {\n        canvas.width = cropWidth;\n        canvas.height = cropHeight;\n        ctx.fillStyle = fillColor;\n        ctx.fillRect(0, 0, cropWidth, cropHeight);\n\n        ctx.drawImage(\n          imgSource,\n          cropX,\n          cropY,\n          cropWidth,\n          cropHeight,\n          0,\n          0,\n          cropWidth,\n          cropHeight,\n        );\n      }\n\n      return canvas;\n    },\n    [fillColor, rotationSlider],\n  );\n\n  /**\n   * upload\n   */\n  const [modalOpen, setModalOpen] = useState(false);\n  const [modalImage, setModalImage] = useState('');\n  const onCancel = useRef<ModalProps['onCancel']>(undefined);\n  const onOk = useRef<ModalProps['onOk']>(undefined);\n\n  const runBeforeUpload = useCallback(\n    async ({\n      beforeUpload,\n      file,\n      resolve,\n      reject,\n    }: {\n      beforeUpload: BeforeUpload;\n      file: RcFile;\n      resolve: (parsedFile: BeforeUploadReturnType) => void;\n      reject: (rejectErr: BeforeUploadReturnType) => void;\n    }) => {\n      const rawFile = file as unknown as File;\n\n      if (typeof beforeUpload !== 'function') {\n        resolve(rawFile);\n        return;\n      }\n\n      try {\n        // https://ant.design/components/upload-cn#api\n        // https://github.com/ant-design/ant-design/blob/master/components/upload/Upload.tsx#L152-L178\n        const result = await beforeUpload(file, [file]);\n\n        if (result === false) {\n          resolve(false);\n        } else {\n          resolve((result !== true && result) || rawFile);\n        }\n      } catch (err) {\n        reject(err as BeforeUploadReturnType);\n      }\n    },\n    [],\n  );\n\n  const getNewBeforeUpload = useCallback(\n    (beforeUpload: BeforeUpload) => {\n      return ((file, fileList) => {\n        return new Promise(async (resolve, reject) => {\n          let processedFile = file;\n\n          if (typeof cb.current.beforeCrop === 'function') {\n            try {\n              const result = await cb.current.beforeCrop(file, fileList);\n              if (result === false) {\n                return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal\n              }\n              if (result !== true) {\n                processedFile = (result as unknown as RcFile) || file; // will open modal\n              }\n            } catch (err) {\n              return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal\n            }\n          }\n\n          // read file\n          const reader = new FileReader();\n          reader.addEventListener('load', () => {\n            if (typeof reader.result === 'string') {\n              setModalOpen(true);\n              setTimeout(() => {\n                setModalImage(reader.result as string);\n              }, 10);\n            }\n          });\n          reader.readAsDataURL(processedFile as unknown as Blob);\n\n          // on modal cancel\n          onCancel.current = () => {\n            setModalOpen(false);\n            setModalImage('');\n            easyCropRef.current!.onReset();\n\n            let hasResolveCalled = false;\n\n            cb.current.onModalCancel?.((LIST_IGNORE) => {\n              resolve(LIST_IGNORE);\n              hasResolveCalled = true;\n            });\n\n            if (!hasResolveCalled) {\n              resolve(AntUpload.LIST_IGNORE);\n            }\n          };\n\n          // on modal confirm\n          onOk.current = async (event: MouseEvent<HTMLElement>) => {\n            setModalOpen(false);\n            setModalImage('');\n            easyCropRef.current!.onReset();\n\n            const canvas = getCropCanvas(event.target as ShadowRoot);\n            const { type, name, uid } = processedFile as UploadFile;\n\n            canvas.toBlob(\n              async (blob) => {\n                const newFile = new File([blob as BlobPart], name, { type });\n                Object.assign(newFile, { uid });\n\n                runBeforeUpload({\n                  beforeUpload,\n                  file: newFile as unknown as RcFile,\n                  resolve: (file) => {\n                    resolve(file);\n                    cb.current.onModalOk?.(file);\n                  },\n                  reject: (err) => {\n                    reject(err);\n                    cb.current.onModalOk?.(err);\n                  },\n                });\n              },\n              type,\n              quality,\n            );\n          };\n        });\n      }) as BeforeUpload;\n    },\n    [getCropCanvas, quality, runBeforeUpload],\n  );\n\n  const getNewUpload = useCallback(\n    (children: ReactNode) => {\n      const upload = Array.isArray(children) ? children[0] : children;\n      const { beforeUpload, accept, ...restUploadProps } = upload.props;\n\n      return {\n        ...upload,\n        props: {\n          ...restUploadProps,\n          accept: accept || 'image/*',\n          beforeUpload: getNewBeforeUpload(beforeUpload),\n        },\n      };\n    },\n    [getNewBeforeUpload],\n  );\n\n  /**\n   * modal\n   */\n  const modalBaseProps = useMemo(() => {\n    const obj: Pick<ModalProps, 'width' | 'okText' | 'cancelText'> = {};\n    if (modalWidth !== undefined) obj.width = modalWidth;\n    if (modalOk !== undefined) obj.okText = modalOk;\n    if (modalCancel !== undefined) obj.cancelText = modalCancel;\n    return obj;\n  }, [modalCancel, modalOk, modalWidth]);\n\n  const wrapClassName = `${PREFIX}-modal${\n    modalClassName ? ` ${modalClassName}` : ''\n  }`;\n\n  const lang = typeof window === 'undefined' ? '' : window.navigator.language;\n  const isCN = lang === 'zh-CN';\n  const title = modalTitle || (isCN ? '编辑图片' : 'Edit image');\n  const resetBtnText = resetText || (isCN ? '重置' : 'Reset');\n\n  return (\n    <>\n      {getNewUpload(children)}\n      <AntModal\n        {...modalProps}\n        {...modalBaseProps}\n        open={modalOpen}\n        title={title}\n        onCancel={onCancel.current}\n        onOk={onOk.current}\n        wrapClassName={wrapClassName}\n        destroyOnHidden\n        {...(isGeThan(version, '6.3.1')\n          ? { mask: { closable: false } }\n          : { maskClosable: true })}\n      >\n        <EasyCrop\n          ref={easyCropRef}\n          cropperRef={cropperRef}\n          zoomSlider={zoomSlider}\n          rotationSlider={rotationSlider}\n          aspectSlider={aspectSlider}\n          showReset={showReset}\n          resetBtnText={resetBtnText}\n          modalImage={modalImage}\n          aspect={aspect}\n          minZoom={minZoom}\n          maxZoom={maxZoom}\n          minAspect={minAspect}\n          maxAspect={maxAspect}\n          cropShape={cropShape}\n          showGrid={showGrid}\n          cropperProps={cropperProps}\n        />\n      </AntModal>\n    </>\n  );\n});\n\nexport default ImgCrop;\n"
  },
  {
    "path": "src/constants.ts",
    "content": "export const PREFIX = 'img-crop';\n\nexport const ZOOM_INITIAL = 1;\nexport const ZOOM_STEP = 0.1;\n\nexport const ROTATION_INITIAL = 0;\nexport const ROTATION_MIN = -180;\nexport const ROTATION_MAX = 180;\nexport const ROTATION_STEP = 1;\n\nexport const ASPECT_STEP = 0.01;\n"
  },
  {
    "path": "src/types.ts",
    "content": "import type { ModalProps, UploadProps } from 'antd';\nimport { ForwardedRef, JSX, MutableRefObject } from 'react';\nimport type { default as Cropper, CropperProps } from 'react-easy-crop';\nimport type { Area } from 'react-easy-crop';\n\nexport type BeforeUpload = Exclude<UploadProps['beforeUpload'], undefined>;\nexport type BeforeUploadReturnType = ReturnType<BeforeUpload>;\n\nexport type ImgCropProps = {\n  quality?: number;\n  fillColor?: string;\n\n  zoomSlider?: boolean;\n  rotationSlider?: boolean;\n  aspectSlider?: boolean;\n  showReset?: boolean;\n  resetText?: string;\n\n  aspect?: number;\n  minZoom?: number;\n  maxZoom?: number;\n  minAspect?: number;\n  maxAspect?: number;\n  cropShape?: 'rect' | 'round';\n  showGrid?: boolean;\n  cropperProps?: Omit<\n    CropperProps,\n    | 'image'\n    | 'crop'\n    | 'zoom'\n    | 'rotation'\n    | 'aspect'\n    | 'minZoom'\n    | 'maxZoom'\n    | 'minAspect'\n    | 'maxAspect'\n    | 'zoomWithScroll'\n    | 'cropShape'\n    | 'showGrid'\n    | 'onCropChange'\n    | 'onZoomChange'\n    | 'onRotationChange'\n    | 'onCropComplete'\n    | 'classes'\n    | 'keyboardStep'\n  > &\n    Partial<Pick<CropperProps, 'keyboardStep'>>;\n\n  modalClassName?: string;\n  modalTitle?: string;\n  modalWidth?: number | string;\n  modalOk?: string;\n  modalCancel?: string;\n  onModalOk?: (value: BeforeUploadReturnType) => void;\n  onModalCancel?: (resolve: (value: BeforeUploadReturnType) => void) => void;\n  modalProps?: Omit<\n    ModalProps,\n    | 'className'\n    | 'title'\n    | 'width'\n    | 'okText'\n    | 'cancelText'\n    | 'onOk'\n    | 'onCancel'\n    | 'open'\n    | 'visible'\n    | 'wrapClassName'\n    | 'maskClosable'\n    | 'destroyOnHidden'\n  >;\n\n  beforeCrop?: BeforeUpload;\n  children: JSX.Element;\n};\n\nexport type EasyCropRef = {\n  rotation: number;\n  cropPixelsRef: MutableRefObject<Area>;\n  onReset: () => void;\n};\n\nexport type EasyCropProps = {\n  cropperRef: ForwardedRef<Cropper>;\n  modalImage: string;\n} & Required<\n  Pick<\n    ImgCropProps,\n    | 'zoomSlider'\n    | 'rotationSlider'\n    | 'aspectSlider'\n    | 'showReset'\n    | 'aspect'\n    | 'minZoom'\n    | 'maxZoom'\n    | 'minAspect'\n    | 'maxAspect'\n    | 'cropShape'\n    | 'showGrid'\n  >\n> &\n  Pick<ImgCropProps, 'cropperProps'> & { resetBtnText: string };\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"alwaysStrict\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitOverride\": true,\n    \"noImplicitReturns\": false,\n    \"noImplicitThis\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"strict\": true,\n    \"strictBindCallApply\": true,\n    \"strictFunctionTypes\": true,\n    \"strictNullChecks\": true,\n    \"skipLibCheck\": true,\n    \"strictPropertyInitialization\": true,\n\n    \"module\": \"nodenext\",\n    \"moduleResolution\": \"nodenext\",\n    \"resolveJsonModule\": true,\n\n    \"outDir\": \"dist\",\n\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"react\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"target\": \"es6\"\n  },\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  }
]