# use-modal-ref

<div align="center">

![npm version](https://img.shields.io/npm/v/use-modal-ref.svg)
![npm downloads](https://img.shields.io/npm/dm/use-modal-ref.svg)
![license](https://img.shields.io/npm/l/use-modal-ref.svg)
[![Test](https://github.com/gxlmyacc/use-modal-ref/actions/workflows/test.yml/badge.svg)](https://github.com/gxlmyacc/use-modal-ref/actions/workflows/test.yml)
![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?logo=vitest&logoColor=white)
![typescript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)
![react](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)

**🚀 Powerful React hooks for elegant modal/drawer management**

[English](https://github.com/gxlmyacc/use-modal-ref/blob/master/README.md) | [中文](https://github.com/gxlmyacc/use-modal-ref/blob/master/README_CN.md)

</div>

## 💡 Why this library?

Introducing `async/await` to JavaScript was a major step forward: nested callback pyramids could be written in a linear, almost synchronous style, avoiding callback hell.

With React, Vue, and other MVVM frameworks, modals are usually controlled by `visible` / `open` props. A business flow like “open dialog → wait for user → continue based on result” is one logical sequence, but it gets split into:

- Parent state for `visible`
- Follow-up logic inside `onOk` / `onCancel` / `onClose`
- Multiple chained dialogs pushing logic into nested callbacks or ad-hoc state machines

**Modal interaction became callback-driven again at the UI layer.**

`use-modal-ref` brings **modal-driven workflows** back to `async/await`: callers `await modalRef.current.modal(...)`, components finish with `endModal` / `cancelModal`, so a full business path stays linear instead of being chopped into scattered handlers.

### Flow comparison

**Traditional approach: state + callbacks split the flow**

```mermaid
flowchart TD
  A[User clicks Submit] --> B["setVisible(true)"]
  B --> C[Modal renders]
  C --> D{User action}
  D -->|OK| E["onOk → read form"]
  D -->|Cancel| F["onCancel"]
  E --> G["setVisible(false)"]
  F --> G
  G --> H["Continue in callback<br/>(API call / open next modal…)"]
  H --> I["More modals → deeper nesting"]
```

**With use-modal-ref: write the flow in order**

```mermaid
flowchart TD
  A[User clicks Submit] --> B["result = await modalRef.modal(data)"]
  B --> C[Modal opens, Promise pending]
  C --> D{User action}
  D -->|OK| E["modal.endModal(result)"]
  D -->|Cancel| F["modal.cancelModal()"]
  E --> G[Promise resolves]
  F --> H[Promise rejects / undefined]
  G --> I["Code after await runs next"]
  H --> I
  I --> J["await another modal in the same function<br/>linear end-to-end flow"]
```

### Code comparison

<details>
<summary>Traditional: visible state + callbacks</summary>

```jsx
function OrderPage() {
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [editOpen, setEditOpen] = useState(false);
  const [draft, setDraft] = useState(null);

  const handleSubmit = () => {
    setConfirmOpen(true); // next steps must live in onOk
  };

  const handleConfirmOk = async () => {
    setConfirmOpen(false);
    const saved = await saveOrder(draft);
    setDraft(saved);
    setEditOpen(true); // another modal → another handler
  };

  const handleEditOk = () => {
    setEditOpen(false);
    message.success('Done'); // step three in yet another callback
  };

  return (
    <>
      <Button onClick={handleSubmit}>Submit order</Button>
      <ConfirmModal open={confirmOpen} onOk={handleConfirmOk} onCancel={() => setConfirmOpen(false)} />
      <EditModal open={editOpen} onOk={handleEditOk} onCancel={() => setEditOpen(false)} />
    </>
  );
}
```

</details>

<details>
<summary>use-modal-ref: chain with await</summary>

```jsx
function OrderPage() {
  const confirmRef = useRef(null);
  const editRef = useRef(null);
  const [draft, setDraft] = useState(null);

  const handleSubmit = async () => {
    const ok = await confirmRef.current.modal({ order: draft });
    if (ok === undefined) return;

    const saved = await saveOrder(ok);
    const edited = await editRef.current.modal(saved);
    if (edited !== undefined) {
      message.success('Done');
    }
  };

  return (
    <>
      <Button onClick={handleSubmit}>Submit order</Button>
      <ConfirmModal ref={confirmRef} />
      <EditModal ref={editRef} />
    </>
  );
}
```

</details>

### When it helps

- **Process-heavy** UX: wizards, confirm-then-edit, double confirm before delete
- You want one **async function** for open → wait → branch → continue, not many `visible` flags and handlers
- Imperative APIs (e.g. `showRefModal`) that still need an **awaitable result**

> Note: Modal UI stays declarative (`visible` / `{...modal.props}`). This library changes how **callers structure business flow**—from callback-driven back to await-driven.

## ✨ Features

- 🎯 **Simple & Intuitive** - Easy-to-use hooks for modal/drawer management
- 🔄 **Async/Await Support** - Return values from modals with Promise-based API
- 🎨 **Framework Agnostic** - Works with any UI library (Antd, Material-UI, etc.)
- 🔧 **Extensible** - Support custom modal types (Popover, Tooltip, etc.)
- 📦 **Lightweight** - Minimal runtime deps (`@babel/runtime` only; React as peer), no UI library dependencies, TypeScript support
- 🎭 **Multiple Usage Patterns** - Ref-based, function-based, and component-based approaches

## 📦 Installation

```bash
npm install use-modal-ref
```

```bash
yarn add use-modal-ref
```

```bash
pnpm add use-modal-ref
```

### Local example (Playground)

Runnable demo: [`examples/`](./examples) (**Vite + Ant Design 5**, sidebar + five README examples).

Dependencies and scripts live in the **root** `package.json`:

```bash
yarn install
yarn demo
```

> Demo deps and scripts live in the root `package.json`. Vite aliases to `src/` for hot reload. Run `yarn build` and use `"use-modal-ref": "file:.."` to validate the npm package layout.

### React version compatibility

| Item | Notes |
|------|-------|
| **peerDependencies** | `react`, `react-dom` **>= 16.8.0** (minimum for Hooks) |
| **Verified** | React **16** (unit tests), **18** (`examples` demo) |
| **React 19** | Uses only stable Hooks APIs (`useState`, `useEffect`, `useMemo`, `useCallback`, `useImperativeHandle`); no legacy context or string refs. No known React 19 incompatibilities—please open an Issue if you hit one |
| **Strict Mode** | Supported; demo runs inside `<React.StrictMode>` |
| **UI libraries** | Works with Ant Design, MUI, etc.; for Ant Design 5+, call `mergeModalType({ drawer: { visible: 'open', onClose: 'onClose' } })` at app entry |

### Browser compatibility

The package ships **two JS builds**. Choose the entry that matches your browser support matrix:

| Entry | Build | Syntax target | Minimum (reference) |
|-------|-------|---------------|---------------------|
| `use-modal-ref` (default) | `esm/` | ES6 | **Chrome ≥ 49** and equivalent baselines |
| `use-modal-ref/es` | `es/` | ES2020 | **Chrome ≥ 87** and equivalent modern browsers |

```js
// Default — wider support (ES6)
import useModalRef from 'use-modal-ref';

// Modern — smaller output, newer browsers only (ES2020)
import useModalRef from 'use-modal-ref/es';
```

> CI validates both builds with `es-check` (`esm/` → ES6, `es/` → ES2020). Your bundler targets and polyfills still apply on top of these baselines.

Feature-specific Web APIs:

| Feature | Required APIs |
|---------|---------------|
| Hooks (`useModalRef`, etc.) | React client runtime only — no DOM |
| `createRefComponent` / `showRefModal` | DOM + `react-dom` |
| `createUrlListener` / `getUrlListener` | `window` + History API by default; **`mergeNativeAdapter({ urlListener })`** for other runtimes |

### Runtime: Web SSR / RSC / React Native

Hooks (`useModalRef` / `useDrawerRef` / `useCommonRef`) depend only on the **React client runtime** (`useState`, `useEffect`, etc.) and **do not touch** `window` / `document`. They work in **browsers, React Native**, and any client environment that supports Hooks.

`createRefComponent` / `showRefModal` default to **DOM + react-dom** (mount adapter injectable). URL listening defaults to **History API** (noop without DOM). On React Native, use **`mergeNativeAdapter`** to declare capabilities (`null` = explicitly unsupported, `undefined` = keep Web default).

**Server Components** (Next.js App Router) cannot use Hooks regardless of platform. The main entry is marked `'use client'` for framework boundaries.

| API | Web browser | React Native | SSR / RSC |
|-----|-------------|--------------|-----------|
| `useModalRef` / `useDrawerRef` / `useCommonRef` | ✅ | ✅ (bind RN `Modal`, etc.) | ✅ in Client Components |
| `createRefComponent` / `showRefModal` | ✅ | ✅ (`mergeNativeAdapter` / `createInTreeMountAdapter`) | ❌ without DOM + default adapter |
| `createUrlListener` / `getUrlListener` | ✅ | ✅ (`mergeNativeAdapter({ urlListener })`) | default noop; `urlListener: null` = permanent noop |
| `mergeModalType` | ✅ | ✅ | ✅ in Server Components |
| `isBrowser()` | usually `true` | usually `false` | `false` on SSR |

> `isBrowser()` means **DOM is available** (`window` + `document`), **not** “Hooks are allowed”. On React Native it is `false`, but Hooks still work.

**React Native example (declarative modal + await):**

```tsx
// React Native only — for Web use antd / MUI (see Quick Start above)
import React from 'react';
import { Modal, View, Text, Button } from 'react-native';
import useModalRef from 'use-modal-ref';

const AppModal = React.forwardRef((_, ref) => {
  const { modal, data } = useModalRef(ref, { title: 'Hello' });

  return (
    <Modal
      visible={modal.props.visible}
      onRequestClose={modal.props.onCancel}
    >
      <View>
        <Text>{data.title}</Text>
        <Button title="OK" onPress={() => modal.endModal('ok')} />
      </View>
    </Modal>
  );
});
```

**React Native / non-DOM: `mergeNativeAdapter`**

```tsx
import {
  mergeNativeAdapter,
  createInTreeMountAdapter,
  showRefModal,
} from 'use-modal-ref';

// Declare native capabilities at startup (null = explicitly unsupported)
mergeNativeAdapter({
  id: 'react-native',
  refComponentMount: myMountAdapter,
  urlListener: myUrlAdapter,
});

const { adapter: inTreeAdapter, Host: RefMountHost } = createInTreeMountAdapter();

function App() {
  useEffect(() => {
    mergeNativeAdapter({ refComponentMount: inTreeAdapter });
    return () => mergeNativeAdapter(null);
  }, []);

  return (
    <>
      <RefMountHost />
      <Main />
    </>
  );
}
```

Legacy **`mergeRefComponentMount`** / **`mergeUrlListenerAdapter`** still work (they delegate to `mergeNativeAdapter`).

**URL listener (`closeWhenUrlChange`):**

```tsx
import { mergeNativeAdapter, showRefModal } from 'use-modal-ref';

mergeNativeAdapter({
  urlListener: {
    id: 'react-navigation',
    createUrlListener: () => ({
      addListener(listener) {
        return navigation.addListener('state', () => listener(getCurrentRouteUrl()));
      },
    }),
  },
});

void showRefModal(MyModal, data, { closeWhenUrlChange: true, urlListenerAdapter: myAdapter });
```

**Next.js App Router example:**

```tsx
// app/layout.tsx — Server Component, global config only
import { mergeModalType } from 'use-modal-ref/config';

mergeModalType({ drawer: { visible: 'open', onClose: 'onClose' } });
```

```tsx
'use client';

import useModalRef from 'use-modal-ref';

const UserModal = React.forwardRef((_, ref) => {
  const { modal, data } = useModalRef(ref, { title: 'User' });
  return <Modal {...modal.props} title={data.title} />;
});
```

```tsx
'use client';

import { showRefModal, isBrowser } from 'use-modal-ref';

function Page() {
  const open = () => {
    if (!isBrowser()) return;
    void showRefModal(MyModal, { title: 'Hello' });
  };
  return <button type="button" onClick={open}>Open</button>;
}
```

> **React 16+**: `'use client'` is ignored as a plain string on React 16/17 (CRA, Vite, etc.). Environment checks use only `typeof window` / `typeof document` — no React 18+ APIs.

## 🚀 Quick Start

### Basic Modal Usage

```jsx
import React, { useState, useRef } from 'react';
import { Modal, Button, Input } from 'antd';
import useModalRef from 'use-modal-ref';

// Modal Component
const TestModal = React.forwardRef((props, ref) => {
  const [inputValue, setInputValue] = useState('');

  const { modal, data } = useModalRef(ref, {
    title: 'Default Title',
    label: 'Default Label'
  });

  const handleOK = () => modal.endModal(inputValue);
  const handleCancel = () => modal.cancelModal();

  return (
    <Modal
      {...modal.props}
      title={data.title}
      footer={[
        <Button key="cancel" onClick={handleCancel}>Cancel</Button>,
        <Button key="ok" type="primary" onClick={handleOK}>OK</Button>
      ]}
    >
      <div>{data.label}</div>
      <Input 
        value={inputValue} 
        onChange={e => setInputValue(e.target.value)} 
        placeholder="Enter value..."
      />
    </Modal>
  );
});

// Usage Component
function App() {
  const modalRef = useRef(null);

  const showModal = async () => {
    const result = await modalRef.current.modal({
      title: 'Custom Title',
      label: 'Please enter a value:'
    });
    
    if (result !== undefined) {
      alert(`You entered: ${result}`);
    }
  };

  return (
    <div>
      <Button type="primary" onClick={showModal}>
        Show Modal
      </Button>
      <TestModal ref={modalRef} />
    </div>
  );
}
```

## 📚 Usage Examples

### 1. Modal with Antd
<details>
<summary>Click to expand</summary>

```jsx
import React, { useState, useRef } from 'react';
import { Modal, Button, Input, Form } from 'antd';
import useModalRef from 'use-modal-ref';

const UserModal = React.forwardRef((props, ref) => {
  const [form] = Form.useForm();
  const [loading, setLoading] = useState(false);

  const { modal, data } = useModalRef(ref, {
    title: 'Add User',
    user: null
  }, {
    // You can perform initialization operations in this event, and the function's return value will be used as the new data returned from the useModalRef hook's data property
    beforeModal({ user }) {
      form.setFieldsValue(user || {
        name: '',
        email: '',
      });
    },
    // You can perform cleanup operations in this event, and the function's return value will be used as the new data returned from the useModalRef hook's data property.
    afterModalClose() {
     setLoading(false);
      form.setFieldsValue({
        name: '',
        email: '',
      });
    }
  });

  const handleSubmit = async () => {
    try {
      setLoading(true);
      const values = await form.validateFields();
      modal.endModal(values);
    } catch (error) {
      console.error('Validation failed:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleCancel = () => modal.cancelModal();

  return (
    <Modal
      {...modal.props}
      title={data.title}
      confirmLoading={loading}
      onOk={handleSubmit}
      onCancel={handleCancel}
    >
      <Form form={form} layout="vertical">
        <Form.Item
          name="name"
          label="Name"
          rules={[{ required: true, message: 'Please enter name' }]}
        >
          <Input placeholder="Enter name" />
        </Form.Item>
        <Form.Item
          name="email"
          label="Email"
          rules={[
            { required: true, message: 'Please enter email' },
            { type: 'email', message: 'Please enter valid email' }
          ]}
        >
          <Input placeholder="Enter email" />
        </Form.Item>
      </Form>
    </Modal>
  );
});

// Usage
function UserManagement() {
  const userModalRef = useRef(null);

  const addUser = async () => {
    const userData = await userModalRef.current.modal({
      title: 'Add New User'
    });
    
    if (userData) {
      console.log('New user:', userData);
      // Handle user creation
    }
  };

  return (
    <div>
      <Button type="primary" onClick={addUser}>
        Add User
      </Button>
      <UserModal ref={userModalRef} />
    </div>
  );
}
```

</details>


### 2. Drawer with Antd

By default, the `drawer` type maps to `visible` + `onClose`, which matches **Ant Design 4** `Drawer`.  
**Ant Design 5+** uses `open` for visibility—call `mergeModalType` once at app entry (see example below).

<details>
<summary>Click to expand</summary>

```jsx
import React, { useState, useRef } from 'react';
import { Drawer, Button, Input, Space } from 'antd';
import { useDrawerRef, mergeModalType } from 'use-modal-ref';

// Ant Design 5+: map drawer visibility to `open` (run once at app entry)
mergeModalType({
  drawer: {
    visible: 'open',
    onClose: 'onClose',
  },
});

const SettingsDrawer = React.forwardRef((props, ref) => {
  const [settings, setSettings] = useState({});

  const { modal, data } = useDrawerRef(ref, {
    title: 'Settings',
    initialSettings: {}
  }, {
    beforeModal: async (data) => {
      setSettings(data.initialSettings);
      return data;
    }
  });

  const handleSave = () => {
    modal.endModal(settings);
  };

  const handleCancel = () => {
    modal.cancelModal();
  };

  // modal.props maps to { open, onClose } after mergeModalType above
  return (
    <Drawer
      {...modal.props}
      title={data.title}
      width={400}
      footer={
        <Space>
          <Button onClick={handleCancel}>Cancel</Button>
          <Button type="primary" onClick={handleSave}>Save</Button>
        </Space>
      }
    >
      <Space direction="vertical" style={{ width: '100%' }}>
        <Input
          placeholder="Setting 1"
          value={settings.setting1 || ''}
          onChange={e => setSettings(prev => ({ ...prev, setting1: e.target.value }))}
        />
        <Input
          placeholder="Setting 2"
          value={settings.setting2 || ''}
          onChange={e => setSettings(prev => ({ ...prev, setting2: e.target.value }))}
        />
      </Space>
    </Drawer>
  );
});

// Usage
function SettingsPage() {
  const settingsRef = useRef(null);

  const openSettings = async () => {
    const newSettings = await settingsRef.current.modal({
      title: 'Edit Settings',
      initialSettings: { setting1: 'value1', setting2: 'value2' }
    });
    
    if (newSettings) {
      console.log('Updated settings:', newSettings);
    }
  };

  return (
    <div>
      <Button onClick={openSettings}>Open Settings</Button>
      <SettingsDrawer ref={settingsRef} />
    </div>
  );
}
```

</details>

### 3. Custom overlay with explicit `modal.props` binding

When your overlay is **not** Ant Design `Modal` / `Drawer` (or does not accept a single spread object), bind visibility and close explicitly instead of `{...modal.props}`:

- `modal.props.visible` — whether the overlay is open
- `modal.props.onCancel` — wired to `modal.cancelModal()` (the awaited `modal()` Promise **rejects** on cancel)

> Runnable demo: `yarn demo` → **Modal props binding** (`examples/src/demos/ModalPropsBindingDemo.tsx`, component `examples/src/components/PropsBindingModal.tsx`).

<details>
<summary>Click to expand</summary>

```tsx
import React, { useRef, useState } from 'react';
import useModalRef, { ModalRef } from 'use-modal-ref';

type FormData = { title: string; hint: string };

/** Custom overlay — only needs visible + onCancel from modal.props */
function YourOverlay({
  visible,
  onCancel,
  title,
  onOk,
  children,
}: {
  visible: boolean;
  onCancel: () => void;
  title: string;
  onOk?: () => void;
  children: React.ReactNode;
}) {
  if (!visible) return null;
  return (
    <div role="dialog" aria-modal="true">
      <h3>{title}</h3>
      {children}
      <button type="button" onClick={onCancel}>Cancel</button>
      {onOk && <button type="button" onClick={onOk}>OK</button>}
    </div>
  );
}

const PropsBindingModal = React.forwardRef<
  ModalRef<'modal', FormData, string>
>((_, ref) => {
  const [value, setValue] = useState('');

  const { modal, data } = useModalRef(ref, {
    title: 'Default title',
    hint: 'Default hint',
  }, {
    beforeModal: (payload) => {
      setValue('');
      return payload;
    },
  });

  const handleOk = () => {
    const trimmed = value.trim();
    if (!trimmed) return;
    modal.endModal(trimmed);
  };

  return (
    <YourOverlay
      visible={modal.props.visible}
      onCancel={modal.props.onCancel}
      title={data.title}
      onOk={handleOk}
    >
      <p>{data.hint}</p>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onKeyDown={(e) => e.key === 'Enter' && handleOk()}
      />
    </YourOverlay>
  );
});

function Page() {
  const modalRef = useRef<ModalRef<'modal', FormData, string>>(null);

  const openModal = async () => {
    try {
      const result = await modalRef.current!.modal({
        title: 'Edit',
        hint: 'Enter a value',
      });
      console.log('Result:', result);
    } catch {
      console.log('Cancelled');
    }
  };

  return (
    <>
      <button type="button" onClick={openModal}>Open</button>
      <PropsBindingModal ref={modalRef} />
    </>
  );
}
```

**Notes:**

- For built-in type `modal`, `modal.props` is `{ visible, onCancel }` by default — Ant Design users often use `<Modal {...modal.props} />`; custom overlays use the two fields above.
- `ref.current.modal({ title: undefined })` does **not** overwrite `defaultData.title` (explicit `undefined` is skipped when merging open payload).
- Confirm with `modal.endModal(value)`; cancel via overlay close / `modal.props.onCancel` → `cancelModal` → Promise rejection.

</details>

### 4. JavaScript modal with JSDoc types

In `.js` projects, declare modal data and ref types with JSDoc and import `ModalRef` from the package — no `.ts` files required. IDE checks apply when `checkJs` is enabled in `jsconfig.json` / `tsconfig.json`.

> Runnable demo: `yarn demo` → **7. JavaScript + JSDoc** (`#/usage/js-radio-modal`); sources `examples/src/components/RadioModal.js`, `examples/src/demos/JsRadioModalDemo.tsx`.

<details>
<summary>Click to expand</summary>

```jsx
// RadioModal.js
import React, { useState } from 'react';
import { Modal, Radio } from 'antd';
import useModalRef from 'use-modal-ref';

/**
 * @typedef {{
 *   title?: string,
 *   width?: number,
 *   okText?: string,
 *   message: Array<{
 *     value: string,
 *     text: string,
 *   }>,
 * }} RadioModalData
 */

/**
 * ModalRef<P, T, U> — P: overlay type, T: data, U: result from endModal
 * @typedef {import('use-modal-ref').ModalRef<'modal', RadioModalData, string>} RadioModalRef
 */

const RadioModal = React.forwardRef(
  /**
   * @param {Record<string, never>} props
   * @param {React.ForwardedRef<RadioModalRef>} ref
   */
  (props, ref) => {
    /** @type {[string | null, React.Dispatch<React.SetStateAction<string | null>>]} */
    const [selectValue, setSelectValue] = useState(null);

    const {
      modal,
      data: { title, width, okText, message },
    } = useModalRef(ref, /** @type {RadioModalData} */ ({
      title: 'Notice',
      width: 560,
      okText: 'OK',
      message: [],
    }), {
      beforeModal({ message: options }) {
        setSelectValue(options[0]?.value ?? null);
      },
    });

    const handleOk = () => {
      if (selectValue != null) {
        modal.endModal(selectValue);
      }
    };

    return (
      <Modal
        {...modal.props}
        title={title}
        width={width}
        okText={okText}
        onOk={handleOk}
        destroyOnClose
      >
        <Radio.Group
          value={selectValue}
          onChange={(e) => setSelectValue(e.target.value)}
        >
          {message.map((item) => (
            <Radio key={item.value} value={item.value}>{item.text}</Radio>
          ))}
        </Radio.Group>
      </Modal>
    );
  },
);

RadioModal.displayName = 'RadioModal';
export default RadioModal;
```

```jsx
// Page.jsx — caller; import types from RadioModal.js via JSDoc import()
import React, { useRef } from 'react';
import { Button } from 'antd';
import RadioModal from './RadioModal';

function Page() {
  /** @type {React.MutableRefObject<import('./RadioModal').RadioModalRef | null>} */
  const radioModalRef = useRef(null);

  const openRadio = async () => {
    try {
      const picked = await radioModalRef.current.modal(
        /** @type {import('./RadioModal').RadioModalData} */ ({
          title: 'Choose one',
          message: [
            { value: 'a', text: 'Option A' },
            { value: 'b', text: 'Option B' },
          ],
        }),
      );
      console.log('Picked:', picked);
    } catch {
      console.log('Cancelled');
    }
  };

  return (
    <>
      <Button type="primary" onClick={openRadio}>Open</Button>
      <RadioModal ref={radioModalRef} />
    </>
  );
}
```

**JSDoc checklist:**

| Goal | Pattern |
|------|---------|
| Modal **data** shape | `@typedef { ... } RadioModalData` |
| **Ref** type (data + result `U`) | `@typedef {import('use-modal-ref').ModalRef<'modal', RadioModalData, string>} RadioModalRef` |
| `forwardRef` ref param | `@param {React.ForwardedRef<RadioModalRef>} ref` |
| `defaultData` literal | `/** @type {RadioModalData} */ ({ ... })` |
| `useRef` in caller | `@type {React.MutableRefObject<import('./RadioModal').RadioModalRef \| null>}` |
| Reference **data** type in caller | `import('./RadioModal').RadioModalData` |

> Pattern adapted from production `RadioModal.js`: define `@typedef` once in the modal component file; callers reference `import('./RadioModal').RadioModalRef` / `RadioModalData` without redeclaring.

</details>

### 5. Custom Popover Component

<details>
<summary>Click to expand</summary>

```jsx
// usePopoverRef.js
import { useCommonRef, mergeModalType } from 'use-modal-ref';

// Register popover modal type
mergeModalType({
  popover: {
    visible: 'visible',
    onClose: 'onClose',
  }
});

const usePopoverRef = (ref, defaultData, options, deps = []) => 
  useCommonRef('popover', ref, defaultData, options, deps);

export default usePopoverRef;
```

```jsx
// ColorPickerPopover.jsx
import React, { useState } from 'react';
import { Popover, Button, Space } from 'antd';
import usePopoverRef from './usePopoverRef';

const ColorPickerPopover = React.forwardRef((props, ref) => {
  const [selectedColor, setSelectedColor] = useState('#1890ff');

  const { modal, data } = usePopoverRef(ref, {
    title: 'Choose Color',
    colors: ['#1890ff', '#52c41a', '#faad14', '#f5222d']
  });

  const handleColorSelect = (color) => {
    setSelectedColor(color);
    modal.endModal(color);
  };

  return (
    <Popover
      {...modal.props}
      title={data.title}
      content={
        <Space direction="vertical">
          {data.colors.map(color => (
            <Button
              key={color}
              style={{ 
                backgroundColor: color, 
                borderColor: color,
                width: 40,
                height: 40
              }}
              onClick={() => handleColorSelect(color)}
            />
          ))}
        </Space>
      }
    >
      {props.children}
    </Popover>
  );
});

// Usage
function ColorPicker() {
  const colorRef = useRef(null);

  const pickColor = async () => {
    const color = await colorRef.current.modal({
      title: 'Select Color',
      colors: ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1']
    });
    
    if (color) {
      console.log('Selected color:', color);
    }
  };

  return (
    <div>
      <ColorPickerPopover ref={colorRef}>
        <Button onClick={pickColor}>Pick Color</Button>
      </ColorPickerPopover>
    </div>
  );
}
```

</details>

### 6. Function-based Modal

<details>
<summary>Click to expand</summary>

```jsx
import React from 'react';
import { Button } from 'antd';
import TestModal from './TestModal';
import { showRefModal } from 'use-modal-ref';

function App() {
  const showModal = async () => {
    const result = await showRefModal(TestModal, {
      title: 'Dynamic Modal',
      label: 'This modal was created dynamically'
    });
    
    if (result) {
      alert(`Modal result: ${result}`);
    }
  };

  return (
    <div>
      <Button type="primary" onClick={showModal}>
        Show Dynamic Modal
      </Button>
    </div>
  );
}
```

</details>

### 7. Component-based Modal

<details>
<summary>Click to expand</summary>

```jsx
import React from 'react';
import { Button } from 'antd';
import TestModal from './TestModal';
import { createRefComponent } from 'use-modal-ref';

function App() {
  const showModal = async () => {
    const [ref, destroy] = await createRefComponent(TestModal);
    
    try {
      const result = await ref.modal({
        title: 'Temporary Modal',
        label: 'This modal will be destroyed after use'
      });
      
      if (result) {
        alert(`Result: ${result}`);
      }
    } finally {
      destroy(); // Clean up the component
    }
  };

  return (
    <div>
      <Button type="primary" onClick={showModal}>
        Show Temporary Modal
      </Button>
    </div>
  );
}
```

</details>

## 🔧 API Reference

### useModalRef

```typescript
function useModalRef<T = any, U = any, C extends Record<string, any> = {}>(
  ref: React.Ref<any>,
  defaultData?: Partial<T> | (() => Partial<T>),
  options?: ModalRefOption<'modal', T, U, C>,
  deps?: React.DependencyList
): {
  modal: ModalRef<'modal', T, U, C>;
  data: T;
  setData: (newData: T | ((data: T) => T)) => void;
}
```


**Generic parameters:**

- `T extends Record<string, any>` - Shape of modal `data` (defaults to `any`)
- `U = any` - Promise resolve type from `modal()`
- `C extends Record<string, any> = {}` - Type of custom properties in `options.custom`, merged onto `ModalRef`

**Parameters:**

- `ref: React.Ref<any>` - React ref object for controlling modal show/hide. Must be passed to the modal component via `React.forwardRef`
- `defaultData?: Partial<T> | (() => Partial<T>)` - Default data, can be an object or a function returning an object. When using a function, it supports lazy initialization and only executes when needed
- `options?: ModalRefOption<'modal', T, U, C>` - Hook config and lifecycle callbacks; see [ModalRefOption](#modalrefoption)
- `deps?: React.DependencyList` - When deps change, re-syncs lifecycle callbacks and `custom` internally (does not change `modal` identity; lifecycle hooks are not on the public `modal` API—configure them via `options`)

**Returns:**

- `modal: ModalRef<'modal', T, U, C>` - Modal control object with:
  - `visible: boolean` - Read-only visibility flag
  - `data: T` - Read-only current modal data
  - `props: { visible: boolean, onCancel: () => void }` - Spread onto Ant Design `Modal` (`visible` / `onCancel`)
  - `options: ModalRefOption<'modal', T, U, C>` - Read-only merged hook options
  - `modalOptions: ModalModalOptions` - Read-only runtime options from the latest `modal()` call
  - `modalPromise: Promise<U> | null` - Read-only current Promise, `null` when closed
  - `modal(newData?: Partial<T>, options?: ModalModalOptions): Promise<U>` - Open modal and return Promise
  - `endModal(result?: U, onDone?: () => void): Promise<void>` - Confirm close and resolve Promise
  - `cancelModal(ex?: any, onDone?: () => void): Promise<void>` - Cancel; rejects unless `alwaysResolve` is set
- `data: T` - Current modal data (same as `modal.data`)
- `setData: (newData: T | ((data: T) => T)) => void` - Update modal data (object or updater)

### ModalRefOption

The `options` parameter of `useModalRef`, `useDrawerRef`, and `useCommonRef` shares this type (generic `P` matches the overlay type).

```typescript
type ModalRefOption<
  P extends ExtendedModalType,
  T extends Record<string, any>,
  U,
  C extends Record<string, any> = {},
  R = unknown
> = {
  useImperativeHandle?: boolean;
  alwaysResolve?: boolean;
  visibleKey?: string;
  custom?: C;
  beforeModal?: (
    newData: Partial<T>,
    pause: (result: any, isError?: boolean) => void,
    options: Record<string, any>
  ) => void | Partial<T> | Promise<void | Partial<T>>;
  afterModal?: (newData: any, options?: ModalModalOptions) => void | Promise<void>;
  init?: (newData: T, options: Record<string, any>) => void | Promise<void>;
  beforeCloseModal?: (
    next: (ok: any) => void,
    action: ModalAction,
    modal: ModalRef<P, T, U>
  ) => void | Promise<void>;
  afterCloseModal?: (
    newData: T,
    action: ModalAction,
    modal: ModalRef<P, T, U>
  ) => void | Promise<void>;
  [key: string]: any;
}
```

**Properties:**

> Lifecycle hooks (`beforeModal`, `afterModal`, etc.) are **configured via `options` only**—they are not part of the public `modal` / `ref.current` type.

- `useImperativeHandle?: boolean` - Whether to expose `modal` on `ref` via `useImperativeHandle`. Defaults to `true` (only `false` disables it). **Treat as constant**—do not change at runtime after the first render
- `alwaysResolve?: boolean` - When `true`, `cancelModal` resolves the Promise instead of rejecting
- `visibleKey?: string` - Overrides the default visibility prop name from `modalTypeMap`, producing `modal.props.[visibleKey]`. For Ant Design 5 `Drawer`, use `'open'`; see [mergeModalType](#mergemodaltype) for global mapping
- `custom?: C` - Custom properties merged onto `modal`, accessible via `modal.xxx` or `ref.current.xxx`. **Must not use built-in modal property names** (e.g. `modal`, `visible`, `endModal`, `beforeModal`); conflicting keys are ignored with a dev warning

  **custom example:**

  ```tsx
  import React, { useState, useRef } from 'react';
  import { Modal } from 'antd';
  import useModalRef, { ModalRef } from 'use-modal-ref';

  const TestModal = React.forwardRef((props, ref) => {
    const [formData, setFormData] = useState({});
    const resetForm = () => setFormData({});
    const validate = () => Object.keys(formData).length > 0;
    const { modal, data } = useModalRef<{ title: string }, any>(
      ref,
      { title: 'Default Title' },
      { custom: { resetForm, validate } }
    );
    return (
      <Modal {...modal.props} title={data.title}>
        <button onClick={() => modal.resetForm()}>Reset</button>
      </Modal>
    );
  });

  function App() {
    const modalRef = useRef<ModalRef<'modal'>>(null);
    return (
      <>
        <button onClick={() => modalRef.current?.modal({ title: 'Test' })}>Open</button>
        <TestModal ref={modalRef} />
      </>
    );
  }
  ```

- `beforeModal?: (...)` - Pre-open hook. May return `Partial<T>` to merge into data; call `pause(result, isError?)` to abort (`isError: true` throws)
  - `newData` - Data passed to this `modal()` call
  - `pause` - Abort/pause callback
  - `options` - Runtime options for this `modal()` call
- `afterModal?: (newData, options?) => void | Promise<void>` - **Post-open** hook (recommended). Hook-level `options.afterModal` and per-call `modal(data, { afterModal })` each fire **once** and do not replace each other
- `init?: (newData, options) => void | Promise<void>` - **Legacy** post-open callback (older API); kept for compatibility—**prefer `afterModal`** for new code. If both are set, `init` runs first, then `afterModal`
- `beforeCloseModal?: (next, action, modal) => void | Promise<void>` - Pre-close hook; call `next(false)` to prevent closing
  - `action` - `'end'` (confirm) or `'cancel'`
- `afterCloseModal?: (newData, action, modal) => void | Promise<void>` - Post-close hook; return value becomes new `data`

### mergeModalType

Globally override prop field names on `modal.props` (visibility and close callback keys).

```typescript
function mergeModalType(map: Partial<MergeModalTypeInput>): MergeableModalTypeMap

type MergeModalTypeInput = Partial<{
  [K in keyof MergeableModalTypeMap]: ModalTypeItem;
}>;
```

**Parameters:**

- `map: Partial<MergeModalTypeInput>` - Partial map to merge. Covers built-in types and types registered via [module augmentation](#extending-overlay-types-module-augmentation). For Ant Design 5 `Drawer` using `open`:

```typescript
import { mergeModalType } from 'use-modal-ref';

mergeModalType({
  drawer: { visible: 'open', onClose: 'onClose' },
});
// All drawer instances then get modal.props { open, onClose }
```

> For a single hook instance only, set `options.visibleKey` instead of changing the global map.

### useDrawerRef

```typescript
function useDrawerRef<
  T extends Record<string, any> = any,
  U = any,
  C extends Record<string, any> = {}
>(
  ref: React.Ref<any>,
  defaultData?: Partial<T> | (() => Partial<T>),
  options?: ModalRefOption<'drawer', T, U, C>,
  deps?: React.DependencyList
): {
  modal: ModalRef<'drawer', T, U, C>;
  data: T;
  setData: (newData: T | ((data: T) => T)) => void;
}
```

**Generic parameters:** Same `T`, `U`, `C` as [useModalRef](#usemodalref).

**Parameters:**

- `ref: React.Ref<any>` - React ref for drawer show/hide
- `defaultData?: Partial<T> | (() => Partial<T>)` - Default data (object or lazy factory)
- `options?: ModalRefOption<'drawer', T, U, C>` - Hook config; see [ModalRefOption](#modalrefoption)
- `deps?: React.DependencyList` - Re-sync `options` onto `modal` when deps change

**Returns:**

- `modal: ModalRef<'drawer', T, U, C>` - Drawer control object; default `modal.props` is `{ visible, onClose }` (map to `{ open, onClose }` via `mergeModalType` or `visibleKey`)
- `data: T` - Current drawer data
- `setData: (newData: T | ((data: T) => T)) => void` - Update drawer data

> **With Ant Design Drawer:** Built-in `drawer` exposes `modal.props.visible` by default. Ant Design 5+ `Drawer` expects `open`—call `mergeModalType({ drawer: { visible: 'open', onClose: 'onClose' } })` at app entry, or set `visibleKey: 'open'` in hook `options` for a single instance.
### useCommonRef

```typescript
function useCommonRef<
  P extends ModalType = ModalType,
  T extends Record<string, any> = any,
  U = any,
  C extends Record<string, any> = {}
>(
  modalType: P,
  ref: React.Ref<any>,
  defaultData?: Partial<T> | (() => Partial<T>),
  options?: ModalRefOption<P, T, U, C>,
  deps?: React.DependencyList
): {
  modal: ModalRef<P, T, U, C>;
  data: T;
  setData: (newData: T | ((data: T) => T)) => void;
}
```

**Generic parameters:**

- `P extends ModalType` - Overlay type (`'modal'`, `'drawer'`, `'popover'`, or extended via `mergeModalType`)
- `T`, `U`, `C` - Same as [useModalRef](#usemodalref)

**Parameters:**

- `modalType: P` - Overlay type identifier
- `ref: React.Ref<any>` - React ref for show/hide control
- `defaultData?: Partial<T> | (() => Partial<T>)` - Default data (object or lazy factory)
- `options?: ModalRefOption<P, T, U, C>` - Hook config; see [ModalRefOption](#modalrefoption)
- `deps?: React.DependencyList` - Re-sync `options` onto `modal` when deps change

**Returns:**

- `modal: ModalRef<P, T, U, C>` - Overlay control object
- `data: T` - Current data
- `setData: (newData: T | ((data: T) => T)) => void` - Update data
### showRefModal

```typescript
function showRefModal<
  M extends ModalRef<any, any>,
  D extends M extends ModalRef<any, infer D> ? D : Record<string, any>,
  U extends M extends ModalRef<any, any, infer U> ? U : any
>(
  RefModal: React.ForwardRefExoticComponent<React.RefAttributes<M>>,
  modalData?: D,
  options?: {
    /** Component props */
    props?: Record<string, any>;
    /** Method name to call modal, defaults to 'modal' */
    modalMethod?: string;
    /** Method name to cancel modal, defaults to 'cancelModal' */
    cancelMethod?: string;
    /** Modal options */
    modalOptions?: ModalModalOptions;
    /** Whether to automatically close modal when URL changes */
    closeWhenUrlChange?: boolean;
    /** Container selector */
    selector?: string;
    /** Container ID */
    id?: string;
    /** Container element or function returning container */
    container?: HTMLElement | null | (() => HTMLElement);
    /** Container class name */
    className?: string;
    /** Callback after ref is created */
    onRef?: (ref: any, destroy: () => void) => void;
    /** Callback before container is appended, return true to prevent auto append */
    onAppendContainer?: (container: HTMLElement) => void | boolean;
    /** Callback before container is removed, return true to prevent auto remove */
    onRemoveContainer?: (container: HTMLElement) => void | boolean;
    /** Callback before component is destroyed, return true to prevent auto destroy */
    onDestroyComponent?: (container: HTMLElement) => void | boolean;
    /** Destroy delay in milliseconds, defaults to 50 */
    destroyDelay?: number;
  }
): Promise<U>
```

**Parameters:**

- `RefModal: React.ForwardRefExoticComponent<React.RefAttributes<M>>` - Modal component to display, must be wrapped with `React.forwardRef`
- `modalData?: D` - Data to pass to the modal, will be passed as the first argument to the `modal` method
- `options?: object` - Configuration options object with the following properties:
  - `props?: Record<string, any>` - Props to pass to the component
  - `modalMethod?: string` - Method name to call modal, defaults to `'modal'`
  - `cancelMethod?: string` - Method name to cancel modal, defaults to `'cancelModal'`
  - `modalOptions?: ModalModalOptions` - Modal options, will be passed as the second argument to the `modal` method
  - `closeWhenUrlChange?: boolean` - Whether to automatically close modal when URL changes, defaults to `false`
  - `selector?: string` - Container selector for finding existing container element
  - `id?: string` - Container ID for finding or creating container element
  - `container?: HTMLElement | null | (() => HTMLElement)` - Container element or function returning container
  - `className?: string` - Container class name, only used when creating new container
  - `onRef?: (ref: any, destroy: () => void) => void` - Callback function after ref is created
  - `onAppendContainer?: (container: HTMLElement) => void | boolean` - Callback before container is appended to DOM, return `true` to prevent auto append
  - `onRemoveContainer?: (container: HTMLElement) => void | boolean` - Callback before container is removed from DOM, return `true` to prevent auto remove
  - `onDestroyComponent?: (container: HTMLElement) => void | boolean` - Callback before component is destroyed, return `true` to prevent auto destroy
  - `destroyDelay?: number` - Destroy delay in milliseconds, defaults to `50`

**Returns:**

- `Promise<U>` - Returns modal result Promise, will be rejected if user cancels

### createRefComponent

```typescript
function createRefComponent<
  T extends React.ForwardRefExoticComponent<React.RefAttributes<any>>,
  P extends T extends React.ForwardRefExoticComponent<React.RefAttributes<infer P>> ? P : never,
  R extends T extends React.ForwardRefExoticComponent<React.RefAttributes<P & infer R>> ? R : never
>(
  RefComponent: T,
  props?: P | null,
  options?: {
    /** Container selector */
    selector?: string;
    /** Container ID */
    id?: string;
    /** Container element or function returning container */
    container?: HTMLElement | null | (() => HTMLElement);
    /** Container class name */
    className?: string;
    /** Callback after ref is created */
    onRef?: (ref: any, destroy: () => void) => void;
    /** Callback before container is appended, return true to prevent auto append */
    onAppendContainer?: (container: HTMLElement) => void | boolean;
    /** Callback before container is removed, return true to prevent auto remove */
    onRemoveContainer?: (container: HTMLElement) => void | boolean;
    /** Callback before component is destroyed, return true to prevent auto destroy */
    onDestroyComponent?: (container: HTMLElement) => void | boolean;
    /** Destroy delay in milliseconds, defaults to 50 */
    destroyDelay?: number;
  }
): Promise<[ref: R, destroy: () => void]>
```

**Parameters:**

- `RefComponent: T` - Component to create, must be wrapped with `React.forwardRef`
- `props?: P | null` - Props to pass to the component
- `options?: object` - Configuration options object with the following properties:
  - `selector?: string` - Container selector for finding existing container element. If found, will use that container; otherwise creates a new one
  - `id?: string` - Container ID for finding or creating container element. If found, will use that container; otherwise creates a new one and sets this ID
  - `container?: HTMLElement | null | (() => HTMLElement)` - Container element or function returning container. If provided, will use this container directly
  - `className?: string` - Container class name, only used when creating new container
  - `onRef?: (ref: any, destroy: () => void) => void` - Callback function after ref is created, receives ref object and destroy function
  - `onAppendContainer?: (container: HTMLElement) => void | boolean` - Callback function before container is appended to DOM, return `true` to prevent auto append to `document.body`
  - `onRemoveContainer?: (container: HTMLElement) => void | boolean` - Callback function before container is removed from DOM, return `true` to prevent auto remove
  - `onDestroyComponent?: (container: HTMLElement) => void | boolean` - Callback function before component is destroyed, return `true` to prevent auto call to `ReactDOM.unmountComponentAtNode`
  - `destroyDelay?: number` - Destroy delay in milliseconds, defaults to `50`. Used to delay destroy operation to avoid incomplete animations

**Returns:**

- `Promise<[ref: R, destroy: () => void]>` - Returns a Promise that resolves to a tuple:
  - `ref: R` - Component's ref object containing all methods and properties exposed by the component
  - `destroy: () => void` - Destroy function that will unmount the component and remove the container (if container was auto-created)

### ModalRef

`ModalRef` is the type definition for the modal control object, containing the following properties and methods:

```typescript
type ModalRef<
  P extends ModalType = 'modal',
  T extends Record<string, any> = Record<string, any>,
  U = any,
  C extends Record<string, any> = {}
> = {
  /** Read-only property: whether modal is visible */
  readonly visible: boolean;
  /** Read-only property: current modal data */
  readonly data: Partial<Omit<T, 'onCancel'|'onOK'>> & { [key: string]: any };
  /** Read-only property: props to pass to Modal/Drawer component */
  readonly props: ModalPropsTypeMap[P];
  /** Read-only property: current configuration options */
  readonly options: ModalRefOption<P, T, U, C>;
  /** Read-only property: current modal options */
  readonly modalOptions: ModalModalOptions;
  /** Read-only property: current modal Promise */
  readonly modalPromise: null | Promise<any> | PromiseLike<any>;

  /** Opens modal and returns Promise */
  modal(newData: T, options?: ModalModalOptions): Promise<U>;

  /** Ends modal and returns result */
  endModal: (result?: U, onDone?: () => void) => Promise<void>;
  
  /** Cancels modal */
  cancelModal: (ex?: any, onDone?: () => void) => Promise<void>;

  [key: string]: any;
} & {
  [key in keyof C]: C[key];
}
```

**Property Descriptions:**

- `visible: boolean` - Read-only property indicating whether the modal is currently visible
- `data: T` - Read-only property containing the current modal data object
- `props: ModalPropsTypeMap[P]` - Read-only property returning appropriate props based on modal type:
  - For `'modal'` type: `{ visible: boolean, onCancel: () => void }`
  - For `'drawer'` type: `{ visible: boolean, onClose: () => void }` by default; use generic `VK` (e.g. `ModalRef<'drawer', T, U, C, 'open'>`) after `mergeModalType` or `visibleKey: 'open'`
  - For `'popover'` type: `{ visible: boolean, onClose: () => void }`
- `options: ModalRefOption<P, T, U, C>` - Read-only property containing current configuration options
- `modalOptions: ModalModalOptions` - Read-only property containing current modal options
- `modalPromise: Promise<U> | null` - Read-only property containing current modal Promise, `null` if modal is not open

**Method Descriptions:**

- `modal(newData: T, options?: ModalModalOptions): Promise<U>` - Method to open the modal
  - `newData: T` - New data to pass to the modal, will be merged with `defaultData`
  - `options?: ModalModalOptions` - Modal options with the following properties:
    - `modalDataEvent?: boolean` - **Legacy data-payload callbacks** (optional). When `true`, you may pass `onOK` / `onCancel` on the **`newData`** object of `modal(newData, { modalDataEvent: true })`:
      - On open, those keys are **removed from `data`** (so they are not rendered or serialized with business fields).
      - On **`endModal(value)`**, `onOK(value)` runs first; if it returns a value other than `undefined`, that becomes the **`await modal()` resolve value** (before `beforeEndModal`).
      - On **`cancelModal(reason)`**, `onCancel(reason)` runs similarly and may rewrite the **reject reason** (before `beforeCancelModal`).
      - Field names are **`onOK`** (capital `OK`) and **`onCancel`** — not Ant Design’s `onOk`.
      - **New code:** prefer `await modal()` + `beforeEndModal` / `beforeCancelModal` in `modal()` options; use `modalDataEvent` only when migrating old APIs that attached OK/Cancel handlers to the data payload.

      ```tsx
      const result = await modalRef.current!.modal(
        {
          title: 'Confirm',
          onOK: async (value) => `processed-${value}`,
          onCancel: async () => 'user aborted',
        },
        { modalDataEvent: true },
      );
      // endModal('x') → result is 'processed-x'
      // cancelModal()   → Promise rejects with 'user aborted'
      ```
    - `afterModal?: (newData: any, options?: ModalModalOptions) => void | Promise<void>` - Hook after modal opens
    - `beforeCloseModal?: (next: (ok: any) => void, action: ModalAction) => void | Promise<void>` - Hook before close
    - `beforeEndModal?: ModalBeforeEndHandler<U>` - Pre-end hook; may rewrite the resolved value. Receives current `U` from `endModal`, returns `U | undefined | void`
    - `beforeCancelModal?: (reason?: any) => Promise<any>` - Hook before cancel, can modify cancel reason
    - `afterCloseModal?: (newData: any, action: ModalAction, modal: ModalRef<any, any, any>) => void | Promise<void>` - Hook after close
    - `forceVisible?: boolean` - Force show, even if another modal is already open
    - `alwaysResolve?: boolean` - Whether to always resolve the Promise
  - Returns `Promise<U>`, resolves with result, rejects when cancelled
- `endModal(result?: U, onDone?: () => void): Promise<void>` - Ends the modal
  - `result?: U` - Result value to return
  - `onDone?: () => void` - Callback function after completion
- `cancelModal(ex?: any, onDone?: () => void): Promise<void>` - Cancels the modal
  - `ex?: any` - Cancel reason, defaults to `'cancel'`
  - `onDone?: () => void` - Callback function after completion

### getUrlListener

Gets the URL listener instance for listening to browser URL changes.

```typescript
function getUrlListener(): UrlListener

interface UrlListener {
  addListener: (
    listener: (url: string) => void,
    options?: { once?: boolean }
  ) => () => void;
}
```

**Returns:**

- `UrlListener` - URL listener object with the following method:
  - `addListener(listener: (url: string) => void, options?: { once?: boolean }): () => void` - Adds a URL change listener
    - `listener: (url: string) => void` - Listener function called when URL changes, receives new URL as parameter
    - `options?: { once?: boolean }` - Options object, when `once` is `true`, listener executes only once
    - Returns a function to remove the listener

**Usage Example:**

```typescript
import { getUrlListener } from 'use-modal-ref';

const urlListener = getUrlListener();

// Add listener
const removeListener = urlListener.addListener((url) => {
  console.log('URL changed to:', url);
}, { once: true });

// Remove listener
removeListener();
```

## 📘 TypeScript Support

This library is written in TypeScript and provides full type definitions out of the box.

### Exported types

Key types importable from `use-modal-ref`:

| Type | Description |
|------|-------------|
| `ModalRef<P, T, U, C, VK, R>` | Six-parameter ref: overlay type, data, result, custom, visibility key, cancel reason |
| `ModalRefOption<P, T, U, C, R>` | Hook `options` |
| `ModalModalOptions<U, R>` | Runtime `modal()` options |
| `UseCommonRefReturn<P, T, U, C, VK, R>` | Hook return value |
| `ModalCustomMap<C>` / `ResolveCustomMap<C, O>` | Custom keys excluding builtins / inferred from options |
| `ResolveVisibleKey<VK, O>` / `ExtractVisibleKey<O>` | Infer props field from `options.visibleKey` literal |
| `ModalBeforeEndHandler<U>` / `ModalBeforeCancelHandler<R>` | Typed before-end / before-cancel handlers |
| `ExtendedModalType` / `ModalTypeRegistry` | Built-in + augmentable overlay types |
| `MergeModalTypeInput` / `MergeableModalTypeMap` | `mergeModalType` input and map types |

### Generics U / R / VK

```tsx
// U: endModal / modal() Promise result
const ref = useRef<ModalRef<'modal', FormData, SubmitResult>>(null);
const { modal } = useModalRef<FormData, SubmitResult>(ref, defaultData);
await modal.endModal({ ok: true });

// R: beforeCancelModal return type
type CancelPayload = { code: number; message: string };
const cancelRef = useRef<ModalRef<'modal', FormData, SubmitResult, {}, undefined, CancelPayload>>(null);
useModalRef<FormData, SubmitResult, {}, undefined, CancelPayload>(cancelRef, defaultData);

// VK: infer from options.visibleKey literal (ref VK must match inferred value)
const drawerRef = useRef<ModalRef<'drawer', Data, void, {}, 'open'>>(null);
const { modal: drawer } = useDrawerRef(drawerRef, data, { visibleKey: 'open' });
void drawer.props.open;
```

### Extending overlay types (module augmentation)

```typescript
// types/use-modal-ref.d.ts
import type { ModalTypeItem } from 'use-modal-ref';

declare module 'use-modal-ref' {
  interface ModalTypeRegistry {
    sidePanel: ModalTypeItem;
  }
}

// app/setup.ts
import { mergeModalType, useCommonRef } from 'use-modal-ref';

mergeModalType({
  sidePanel: { visible: 'show', onClose: 'hide' },
});

const ref = React.createRef<ModalRef<'sidePanel', { title: string }, boolean>>();
const { modal } = useCommonRef('sidePanel', ref, { title: 'Panel' });
```

### Type Safety

```tsx
interface ModalData {
  title: string;
  label: string;
}

interface ModalResult {
  value: string;
}

const TestModal = React.forwardRef((props, ref) => {
  const { modal, data } = useModalRef<ModalData, ModalResult>(ref, {
    title: 'Default Title',
    label: 'Default Label'
  });
  
  // TypeScript will ensure data conforms to ModalData
  // and modal callbacks return ModalResult
});
```

### Strict Mode Support

The library fully supports React's strict mode and concurrent features.

## ⚠️ Error Handling

Handle potential errors in your modal interactions:

```jsx
const showModal = async () => {
  try {
    const result = await modalRef.current.modal(data);
    if (result !== undefined) {
      // Handle success
      console.log('Modal result:', result);
    } else {
      // Handle cancel/dismiss
      console.log('Modal was cancelled');
    }
  } catch (error) {
    // Handle errors during modal operations
    console.error('Modal error:', error);
  }
};
```

## ⚡ Performance Optimization

### Dependency Array Usage

Use the dependency array to optimize re-renders:

```jsx
const { modal, data } = useModalRef(ref, defaultData, options, [dep1, dep2]);
```

### Lazy Initialization

For expensive initializations, use function form of defaultData:

```jsx
const { modal, data } = useModalRef(ref, () => {
  // Expensive computation only runs when needed
  return {
    title: getLocalizedTitle(),
    items: generateInitialItems()
  };
});
```

## 🎯 Advanced Features

### Custom Modal Types

Register extended overlay types via `ModalTypeRegistry` augmentation + `mergeModalType` (see [module augmentation](#extending-overlay-types-module-augmentation)). You can also merge runtime aliases for built-in keys:

```jsx
import { useCommonRef, mergeModalType } from 'use-modal-ref';

mergeModalType({
  tooltip: {
    visible: 'open',
    onClose: 'onOpenChange',
  }
});

const useTooltipRef = (ref, defaultData, options, deps = []) =>
  useCommonRef('tooltip' as any, ref, defaultData, options, deps);
```

### Before/After Hooks

```jsx
const { modal, data } = useModalRef(ref, defaultData, {
  beforeModal: async (data) => {
    // Called before modal opens
    console.log('Opening modal with data:', data);
    return data; // Can modify data
  },
  afterModal: (result) => {
    // Called after modal closes
    console.log('Modal closed with result:', result);
  }
});
```

## 🤝 Contributing

We welcome all contributions! Here's how you can help:

1. **Report bugs** - Use the issue tracker to report bugs
2. **Suggest features** - Propose new features or improvements
3. **Submit PRs** - Fix bugs or implement new features


## 📄 License

This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.

## ❓ FAQ

### Why use refs instead of state for modal control?

Refs provide direct access to modal methods without triggering re-renders, making the API more efficient and easier to use.

### How to handle multiple modals?

Each modal should have its own ref:

```jsx
function App() {
  const modal1Ref = useRef(null);
  const modal2Ref = useRef(null);
  
  return (
    <>
      <MyModal ref={modal1Ref} />
      <AnotherModal ref={modal2Ref} />
    </>
  );
}
```

### Can I use this with other UI libraries?

Yes! The library is UI-agnostic. Here's an example with Material-UI:

```jsx
import { Dialog, DialogTitle, DialogContent } from '@mui/material';
import useModalRef from 'use-modal-ref';

const MaterialModal = React.forwardRef((props, ref) => {
  const { modal, data } = useModalRef(ref, { title: 'Dialog' });
  
  return (
    <Dialog {...modal.props} open={modal.props.visible}>
      <DialogTitle>{data.title}</DialogTitle>
      <DialogContent>
        {/* Your content */}
      </DialogContent>
    </Dialog>
  );
};
```

