diff --git a/.changeset/spotty-flies-jump.md b/.changeset/spotty-flies-jump.md
new file mode 100644
index 0000000000..3a86369b06
--- /dev/null
+++ b/.changeset/spotty-flies-jump.md
@@ -0,0 +1,6 @@
+---
+"@nextui-org/input-otp": minor
+"@nextui-org/theme": minor
+---
+
+Adding new input-otp component.
diff --git a/apps/docs/config/routes.json b/apps/docs/config/routes.json
index 1d5500f469..8f482940f3 100644
--- a/apps/docs/config/routes.json
+++ b/apps/docs/config/routes.json
@@ -273,6 +273,13 @@
"keywords": "input, text box, form field, data entry",
"path": "/docs/components/input.mdx"
},
+ {
+ "key": "input-otp",
+ "title": "Input OTP",
+ "keywords": "input, otp, auth",
+ "path": "/docs/components/input-otp.mdx",
+ "newPost": true
+ },
{
"key": "kbd",
"title": "Kbd",
diff --git a/apps/docs/content/components/dropdown/variants.raw.jsx b/apps/docs/content/components/dropdown/variants.raw.jsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/docs/content/components/index.ts b/apps/docs/content/components/index.ts
index 45cfacb811..ced4352a6f 100644
--- a/apps/docs/content/components/index.ts
+++ b/apps/docs/content/components/index.ts
@@ -18,6 +18,7 @@ export * from "./user";
export * from "./skeleton";
export * from "./snippet";
export * from "./input";
+export * from "./input-otp";
export * from "./textarea";
export * from "./image";
export * from "./radio-group";
diff --git a/apps/docs/content/components/input-otp/allowed-keys.raw.jsx b/apps/docs/content/components/input-otp/allowed-keys.raw.jsx
new file mode 100644
index 0000000000..a9d2e828aa
--- /dev/null
+++ b/apps/docs/content/components/input-otp/allowed-keys.raw.jsx
@@ -0,0 +1,22 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ const exps = [
+ {
+ name: "For below InputOtp, only lower-case alphabets (a to z) are allowed:",
+ value: "^[a-z]*$",
+ },
+ {name: "For below InputOtp, only upper-case alphabets(A to Z) are allowed:", value: "^[A-Z]*$"},
+ ];
+
+ return (
+
+ {exps.map((exp, idx) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/allowed-keys.ts b/apps/docs/content/components/input-otp/allowed-keys.ts
new file mode 100644
index 0000000000..4b28d9836f
--- /dev/null
+++ b/apps/docs/content/components/input-otp/allowed-keys.ts
@@ -0,0 +1,9 @@
+import App from "./allowed-keys.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/colors.raw.jsx b/apps/docs/content/components/input-otp/colors.raw.jsx
new file mode 100644
index 0000000000..5523609028
--- /dev/null
+++ b/apps/docs/content/components/input-otp/colors.raw.jsx
@@ -0,0 +1,19 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ const colors = ["default", "primary", "secondary", "success", "warning", "danger"];
+
+ return (
+
+ {colors.map((color) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/colors.ts b/apps/docs/content/components/input-otp/colors.ts
new file mode 100644
index 0000000000..d5bef810aa
--- /dev/null
+++ b/apps/docs/content/components/input-otp/colors.ts
@@ -0,0 +1,9 @@
+import App from "./colors.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/controlled.raw.jsx b/apps/docs/content/components/input-otp/controlled.raw.jsx
new file mode 100644
index 0000000000..941963a4f1
--- /dev/null
+++ b/apps/docs/content/components/input-otp/controlled.raw.jsx
@@ -0,0 +1,13 @@
+import {InputOtp} from "@nextui-org/react";
+import React from "react";
+
+export default function App() {
+ const [value, setValue] = React.useState("");
+
+ return (
+
+
+
InputOtp value: {value}
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/controlled.ts b/apps/docs/content/components/input-otp/controlled.ts
new file mode 100644
index 0000000000..2c3f0cacb4
--- /dev/null
+++ b/apps/docs/content/components/input-otp/controlled.ts
@@ -0,0 +1,9 @@
+import App from "./controlled.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/description.raw.jsx b/apps/docs/content/components/input-otp/description.raw.jsx
new file mode 100644
index 0000000000..54ac7feb36
--- /dev/null
+++ b/apps/docs/content/components/input-otp/description.raw.jsx
@@ -0,0 +1,9 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/description.ts b/apps/docs/content/components/input-otp/description.ts
new file mode 100644
index 0000000000..aeb6340b6b
--- /dev/null
+++ b/apps/docs/content/components/input-otp/description.ts
@@ -0,0 +1,9 @@
+import App from "./description.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/disabled.raw.jsx b/apps/docs/content/components/input-otp/disabled.raw.jsx
new file mode 100644
index 0000000000..6af63e7e21
--- /dev/null
+++ b/apps/docs/content/components/input-otp/disabled.raw.jsx
@@ -0,0 +1,9 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/disabled.ts b/apps/docs/content/components/input-otp/disabled.ts
new file mode 100644
index 0000000000..1a215cc91f
--- /dev/null
+++ b/apps/docs/content/components/input-otp/disabled.ts
@@ -0,0 +1,9 @@
+import App from "./disabled.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/error-message.raw.jsx b/apps/docs/content/components/input-otp/error-message.raw.jsx
new file mode 100644
index 0000000000..7dcfffbf1b
--- /dev/null
+++ b/apps/docs/content/components/input-otp/error-message.raw.jsx
@@ -0,0 +1,13 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/error-message.ts b/apps/docs/content/components/input-otp/error-message.ts
new file mode 100644
index 0000000000..fb8101b132
--- /dev/null
+++ b/apps/docs/content/components/input-otp/error-message.ts
@@ -0,0 +1,9 @@
+import App from "./error-message.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/index.ts b/apps/docs/content/components/input-otp/index.ts
new file mode 100644
index 0000000000..2f9f87ce61
--- /dev/null
+++ b/apps/docs/content/components/input-otp/index.ts
@@ -0,0 +1,29 @@
+import usage from "./usage";
+import disabled from "./disabled";
+import readonly from "./readonly";
+import required from "./required";
+import sizes from "./sizes";
+import colors from "./colors";
+import variants from "./variants";
+import radius from "./radius";
+import description from "./description";
+import errorMessage from "./error-message";
+import allowedKeys from "./allowed-keys";
+import controlled from "./controlled";
+import password from "./password";
+
+export const inputOtpContent = {
+ usage,
+ disabled,
+ readonly,
+ required,
+ sizes,
+ colors,
+ variants,
+ radius,
+ description,
+ errorMessage,
+ allowedKeys,
+ controlled,
+ password,
+};
diff --git a/apps/docs/content/components/input-otp/password.raw.jsx b/apps/docs/content/components/input-otp/password.raw.jsx
new file mode 100644
index 0000000000..223d1ec6f0
--- /dev/null
+++ b/apps/docs/content/components/input-otp/password.raw.jsx
@@ -0,0 +1,9 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/password.ts b/apps/docs/content/components/input-otp/password.ts
new file mode 100644
index 0000000000..7751eaf935
--- /dev/null
+++ b/apps/docs/content/components/input-otp/password.ts
@@ -0,0 +1,9 @@
+import App from "./password.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/radius.raw.jsx b/apps/docs/content/components/input-otp/radius.raw.jsx
new file mode 100644
index 0000000000..18ed4174f4
--- /dev/null
+++ b/apps/docs/content/components/input-otp/radius.raw.jsx
@@ -0,0 +1,19 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ const radiusValues = ["none", "sm", "md", "lg", "full"];
+
+ return (
+
+ {radiusValues.map((radius) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/radius.ts b/apps/docs/content/components/input-otp/radius.ts
new file mode 100644
index 0000000000..7b78db1ce0
--- /dev/null
+++ b/apps/docs/content/components/input-otp/radius.ts
@@ -0,0 +1,9 @@
+import App from "./radius.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/readonly.raw.jsx b/apps/docs/content/components/input-otp/readonly.raw.jsx
new file mode 100644
index 0000000000..2d70284f38
--- /dev/null
+++ b/apps/docs/content/components/input-otp/readonly.raw.jsx
@@ -0,0 +1,9 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/readonly.ts b/apps/docs/content/components/input-otp/readonly.ts
new file mode 100644
index 0000000000..fabd05ba36
--- /dev/null
+++ b/apps/docs/content/components/input-otp/readonly.ts
@@ -0,0 +1,9 @@
+import App from "./readonly.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/required.raw.jsx b/apps/docs/content/components/input-otp/required.raw.jsx
new file mode 100644
index 0000000000..30c521e995
--- /dev/null
+++ b/apps/docs/content/components/input-otp/required.raw.jsx
@@ -0,0 +1,9 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/required.ts b/apps/docs/content/components/input-otp/required.ts
new file mode 100644
index 0000000000..b50b781e6f
--- /dev/null
+++ b/apps/docs/content/components/input-otp/required.ts
@@ -0,0 +1,9 @@
+import App from "./required.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/sizes.raw.jsx b/apps/docs/content/components/input-otp/sizes.raw.jsx
new file mode 100644
index 0000000000..b3a3938afe
--- /dev/null
+++ b/apps/docs/content/components/input-otp/sizes.raw.jsx
@@ -0,0 +1,19 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ const sizes = ["sm", "md", "lg"];
+
+ return (
+
+ {sizes.map((size) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/sizes.ts b/apps/docs/content/components/input-otp/sizes.ts
new file mode 100644
index 0000000000..85a2f5b30b
--- /dev/null
+++ b/apps/docs/content/components/input-otp/sizes.ts
@@ -0,0 +1,9 @@
+import App from "./sizes.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/usage.raw.jsx b/apps/docs/content/components/input-otp/usage.raw.jsx
new file mode 100644
index 0000000000..c94601370f
--- /dev/null
+++ b/apps/docs/content/components/input-otp/usage.raw.jsx
@@ -0,0 +1,14 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ const [value, setValue] = React.useState("");
+
+ return (
+
+
+
+ OTP value: {value}
+
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/usage.ts b/apps/docs/content/components/input-otp/usage.ts
new file mode 100644
index 0000000000..1118304c37
--- /dev/null
+++ b/apps/docs/content/components/input-otp/usage.ts
@@ -0,0 +1,9 @@
+import App from "./usage.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/input-otp/variants.raw.jsx b/apps/docs/content/components/input-otp/variants.raw.jsx
new file mode 100644
index 0000000000..b61cb9f2c8
--- /dev/null
+++ b/apps/docs/content/components/input-otp/variants.raw.jsx
@@ -0,0 +1,19 @@
+import {InputOtp} from "@nextui-org/react";
+
+export default function App() {
+ const variants = ["flat", "bordered", "underlined", "faded"];
+
+ return (
+
+ {variants.map((variant) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/docs/content/components/input-otp/variants.ts b/apps/docs/content/components/input-otp/variants.ts
new file mode 100644
index 0000000000..ddea95fb2e
--- /dev/null
+++ b/apps/docs/content/components/input-otp/variants.ts
@@ -0,0 +1,9 @@
+import App from "./variants.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/docs/components/input-otp.mdx b/apps/docs/content/docs/components/input-otp.mdx
new file mode 100644
index 0000000000..e1478727a7
--- /dev/null
+++ b/apps/docs/content/docs/components/input-otp.mdx
@@ -0,0 +1,186 @@
+---
+title: "Input OTP"
+description: "InputOtp is a component that allows users to enter OTP."
+---
+
+import {inputOtpContent} from "@/content/components/input-otp";
+
+# Input OTP
+
+InputOtp is a component that allows users to enter OTP.
+
+---
+
+
+
+## Installation
+
+
+
+
+## Import
+
+
+
+## Usage
+
+
+
+## Disabled
+
+Passing `isDisabled` property will make `input-otp` disabled.
+
+
+
+## Read Only
+
+Passing `isReadOnly` property will make `input-otp` read only.
+
+
+
+### Required
+
+Passing `isRequired` property will make `input-otp` required.
+
+
+
+### Sizes
+
+Size of the `input-otp` can be changed by `size` property. By default, `size` property is set to `md`.
+
+
+
+### Colors
+
+Color of the `input-otp` can be changed by `color` property.
+
+
+
+### Variants
+
+Styling/Variant of the `input-otp` can be changed by `variant` property. By default, `variant` property is set to `flat`.
+
+
+
+### Radius
+
+Radius of the `input-otp` can be changed by `radius` property. By default, `radius` property is set to `md`.
+
+
+
+### Password
+
+InputOtp can be used as password/secured-pin input by setting `type` as `password`.
+
+
+
+### Description
+
+Description of the `input-otp` can be set by `description` property.
+
+
+
+### Error Message
+
+Custom error message of the `input-otp` can be set by `errorMessage` property.
+
+
+
+### Allowed Keys
+
+* Users are only allowed to type certain keys. Any input other than allowed keys is simply ignored.
+* Allowed Keys can be modified by `allowedKeys` property which accepts the regex of the keys which are allowed.
+* By default, the value of `allowedKeys` is `^[0-9]*$` (i.e. only numerical digits are allowed).
+
+
+
+### Controlled
+
+
+
+## Slots
+
+- **base**: InputOtp wrapper, it handles alignment, placement, and general appearance.
+- **wrapper**: Wraps the underlying input-otp component. Sent as `containerClassName` prop to underlying input-otp component.
+- **input**: The input element.
+- **segmentWrapper**: Wraps all the segment elements.
+- **segment**: The segment element.
+- **caret**: The caret represents the typing indicator of the InputOtp component.
+- **passwordChar**: The passwordChar represents the text styling when input-type is password.
+- **helperWrapper**: Wraps the `description` and the `errorMessage`.
+- **description**: The description of the InputOtp.
+- **errorMessage**: The error message of the InputOtp.
+
+
+
+## Data Attributes
+
+`InputOtp` has the following attributes on the `base` element:
+
+- **data-invalid**:
+ When the input-otp is invalid. Based on `isInvalid` prop.
+- **data-required**:
+ When the input-otp is required. Based on `isRequired` prop.
+- **data-readonly**:
+ When the input-otp is readonly. Based on `isReadOnly` prop.
+- **data-filled**:
+ When the input-otp is completely filled.
+- **data-disabled**:
+ When the input is disabled. Based on `isDisabled` prop.
+
+
+
+## Accessibility
+
+- Built on top of [input-otp](https://github.com/guilhermerodz/input-otp).
+- Required and invalid states exposed to assistive technology via ARIA.
+- Support for description and error message help text linked to the input-otp via ARIA.
+
+
+
+## API
+
+### InputOtp Props
+
+| Attribute | Type | Description | Default |
+| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
+| length | `number` | The length of the input-otp. | `4` |
+| allowedKeys | `regEx string` | The allowed keys for the input-otp. | `^[0-9]*$`|
+| variant | `flat` \| `bordered` \| `faded` \| `underlined` | The variant of the input-otp. | `flat` |
+| color | `default` \| `primary` \| `secondary` \| `success` \| `warning` \| `danger` | The color of the input-otp. | `default` |
+| size | `sm` \| `md` \| `lg` | The size of the input-otp. | `md` |
+| radius | `none` \| `sm` \| `md` \| `lg` \| `full` | The radius of the input-otp. | - |
+| value | `string` | The current value of the input-otp (controlled). | - |
+| defaultValue | `string` | The default value of the input-otp (uncontrolled). | - |
+| description | `ReactNode` | A description for the input. Provides a hint such as specific requirements for what to choose. | - |
+| errorMessage | `ReactNode` \| `((v: ValidationResult) => ReactNode)` | An error message for the input-otp. It is only shown when `isInvalid` is set to `true` | - | | The position of the label. | `inside` |
+| fullWidth | `boolean` | Whether the input-otp should take up the width of its parent. | `false` |
+| isRequired | `boolean` | Whether user input-otp is required on the input before form submission. | `false` |
+| isReadOnly | `boolean` | Whether the input-otp can be selected but not changed by the user. | |
+| isDisabled | `boolean` | Whether the input-otp is disabled. | `false` |
+| isInvalid | `boolean` | Whether the input-otp is invalid. | `false` |
+| baseRef | `RefObject` | The ref to the base element. | - |
+| disableAnimation | `boolean` | Whether the input-otp should be animated. | `false` |
+| classNames | `Record<"base"| "inputWrapper"| "input"| "segmentWrapper"| "segment" | "caret" | "passwordChar" | "helperWrapper" | "description" | "errorMessage", string>` | Allows to set custom class names for the Input slots. | - |
+
+### InputOtp Events
+
+| Attribute | Type | Description |
+| ------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| onChange | `React.ChangeEvent` | Handler that is called when the element's value changes. You can pull out the new value by accessing `event.target.value` (string). |
+| onValueChange | `(value: string) => void` | Handler that is called when the element's value changes. |
+| onComplete | `(value: string) => void` | Handler that is called when the element's value is completely filled. |
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 01e4bc4aba..b135d859ac 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -26,6 +26,7 @@
"@nextui-org/divider": "workspace:*",
"@nextui-org/kbd": "workspace:*",
"@nextui-org/listbox": "workspace:*",
+ "@nextui-org/input-otp": "workspace:*",
"@nextui-org/react": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
diff --git a/apps/docs/public/sitemap-0.xml b/apps/docs/public/sitemap-0.xml
index 09725255fe..4d60321842 100644
--- a/apps/docs/public/sitemap-0.xml
+++ b/apps/docs/public/sitemap-0.xml
@@ -1,65 +1,77 @@
-https://nextui.org/feed.xml2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/figma2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/accordion2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/autocomplete2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/avatar2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/badge2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/breadcrumbs2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/button2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/card2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/checkbox-group2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/checkbox2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/chip2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/circular-progress2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/code2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/divider2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/dropdown2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/image2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/input2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/kbd2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/link2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/listbox2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/modal2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/navbar2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/pagination2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/popover2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/progress2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/radio-group2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/scroll-shadow2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/select2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/skeleton2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/slider2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/snippet2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/spacer2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/spinner2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/switch2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/table2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/tabs2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/textarea2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/tooltip2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/components/user2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/frameworks/astro2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/frameworks/nextjs2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/frameworks/remix2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/frameworks/vite2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/guide/design-principles2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/guide/installation2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/guide/introduction2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/guide/routing2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/guide/upgrade-to-v22024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/customization/colors2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/customization/create-theme2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/customization/custom-variants2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/customization/customize-theme2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/customization/dark-mode2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/customization/layout2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/customization/override-styles2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/docs/customization/theme2024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/blog/nextui-v22024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/blog/v2.1.02024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/blog/v2.2.02024-02-02T20:24:45.107Zdaily0.7
-https://nextui.org/blog2024-02-02T20:24:45.107Zdaily0.7
+https://nextui.org/feed.xml2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/figma2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/blog/nextui-v22024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/blog/v2.1.02024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/blog/v2.2.02024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/blog/v2.3.02024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/blog/v2.4.02024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/api-references/cli-api2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/api-references/nextui-provider2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/customization/colors2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/customization/create-theme2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/customization/custom-variants2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/customization/customize-theme2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/customization/dark-mode2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/customization/layout2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/customization/override-styles2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/customization/theme2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/accordion2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/autocomplete2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/avatar2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/badge2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/breadcrumbs2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/button2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/calendar2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/card2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/checkbox-group2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/checkbox2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/chip2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/circular-progress2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/code2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/date-input2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/date-picker2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/date-range-picker2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/divider2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/dropdown2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/image2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/input-otp2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/input2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/kbd2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/link2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/listbox2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/modal2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/navbar2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/pagination2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/popover2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/progress2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/radio-group2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/range-calendar2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/scroll-shadow2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/select2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/skeleton2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/slider2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/snippet2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/spacer2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/spinner2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/switch2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/table2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/tabs2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/textarea2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/time-input2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/tooltip2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/components/user2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/frameworks/astro2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/frameworks/nextjs2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/frameworks/remix2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/frameworks/vite2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/guide/cli2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/guide/design-principles2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/guide/installation2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/guide/introduction2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/guide/routing2024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/docs/guide/upgrade-to-v22024-10-23T19:45:31.239Zdaily0.7
+https://nextui.org/blog2024-10-23T19:45:31.239Zdaily0.7
\ No newline at end of file
diff --git a/packages/components/input-otp/README.md b/packages/components/input-otp/README.md
new file mode 100644
index 0000000000..61442f0d22
--- /dev/null
+++ b/packages/components/input-otp/README.md
@@ -0,0 +1,26 @@
+# @nextui-org/input-otp
+
+InputOTP is a component that allows users to enter otp input. It can be used to get user otp in forms.
+
+This package contains the InputOTPcomponent.
+
+Please refer to the [documentation](https://nextui.org/docs/components/input-otp) for more information.
+
+## Installation
+
+```sh
+yarn add @nextui-org/input-otp
+# or
+npm i @nextui-org/input-otp
+```
+
+## Contribution
+
+Yes please! See the
+[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
+for details.
+
+## License
+
+This project is licensed under the terms of the
+[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).
diff --git a/packages/components/input-otp/__tests__/input-otp.test.tsx b/packages/components/input-otp/__tests__/input-otp.test.tsx
new file mode 100644
index 0000000000..fdde22f4d6
--- /dev/null
+++ b/packages/components/input-otp/__tests__/input-otp.test.tsx
@@ -0,0 +1,195 @@
+import * as React from "react";
+import {render, renderHook, screen} from "@testing-library/react";
+import {useForm} from "react-hook-form";
+import userEvent, {UserEvent} from "@testing-library/user-event";
+
+import {InputOtp} from "../src";
+
+// Mock document.elementFromPoint to avoid test environment errors
+beforeAll(() => {
+ document.elementFromPoint = jest.fn(() => {
+ const mockElement = document.createElement("div");
+
+ return mockElement;
+ });
+});
+
+describe("InputOtp Component", () => {
+ let user: UserEvent;
+
+ beforeAll(() => {
+ user = userEvent.setup();
+ });
+
+ it("should render correctly", () => {
+ const wrapper = render();
+
+ expect(() => wrapper.unmount()).not.toThrow();
+ });
+
+ it("should forward ref correctly", () => {
+ const ref = React.createRef();
+
+ render();
+ expect(ref.current).not.toBeNull();
+ });
+
+ it("should create segments according to length prop", () => {
+ render();
+ const segments = screen.getAllByRole("presentation");
+
+ expect(segments.length).toBe(5);
+ });
+
+ it("should display error message when isInvalid is true", () => {
+ const errorMessage = "custom error message";
+
+ render();
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
+ });
+
+ it("should display description message", () => {
+ const descriptionMessage = "custom description message";
+
+ render();
+ expect(screen.getByText(descriptionMessage)).toBeInTheDocument();
+ });
+
+ it("should not focus when disabled", async () => {
+ render();
+ const input = screen.getByRole("textbox");
+
+ await user.click(input);
+ expect(input).not.toHaveAttribute("data-focus", "true");
+ });
+
+ it("should activate the first segment on click", async () => {
+ render();
+ const input = screen.getByRole("textbox");
+ const segments = screen.getAllByRole("presentation");
+
+ await user.click(input);
+ expect(segments[0]).toHaveAttribute("data-active", "true");
+ expect(segments[1]).not.toHaveAttribute("data-active");
+ });
+
+ it("should move focus to the next segment on valid input", async () => {
+ render();
+ const input = screen.getByRole("textbox");
+ const segments = screen.getAllByRole("presentation");
+
+ await user.click(input);
+ expect(segments[1]).not.toHaveAttribute("data-active");
+
+ await user.keyboard("1");
+ expect(segments[1]).toHaveAttribute("data-active", "true");
+ expect(input).toHaveAttribute("value", "1");
+ });
+
+ it("should clear input on backspace", async () => {
+ render();
+ const input = screen.getByRole("textbox");
+ const segments = screen.getAllByRole("presentation");
+
+ await user.click(input);
+ await user.keyboard("12");
+ expect(input).toHaveAttribute("value", "12");
+ expect(segments[2]).toHaveAttribute("data-active", "true");
+
+ await user.keyboard("[Backspace]");
+ expect(input).toHaveAttribute("value", "1");
+ expect(segments[1]).toHaveAttribute("data-active", "true");
+ });
+
+ it("should paste values", async () => {
+ render();
+ const input = screen.getByRole("textbox");
+
+ await user.click(input);
+ await user.paste("1234");
+ expect(input).toHaveAttribute("value", "1234");
+ });
+
+ it("should restrict non-allowed inputs", async () => {
+ render();
+ const input = screen.getByRole("textbox");
+ const segments = screen.getAllByRole("presentation");
+
+ await user.click(input);
+ await user.keyboard("a");
+ expect(input).toHaveAttribute("value", "");
+ expect(segments[0]).toHaveAttribute("data-active", "true");
+ });
+
+ it("should allow inputs based on custom regex", async () => {
+ const regEx = "^[a-z]*$";
+
+ render();
+ const input = screen.getByRole("textbox");
+
+ await user.click(input);
+ await user.keyboard("a");
+ expect(input).toHaveAttribute("value", "a");
+ });
+
+ it("should call onComplete when all segments are filled", async () => {
+ const onComplete = jest.fn();
+
+ render();
+ const input = screen.getByRole("textbox");
+
+ await user.click(input);
+ await user.paste("1234");
+ expect(onComplete).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe("InputOtp with react-hook-form", () => {
+ let user: UserEvent;
+
+ beforeAll(() => {
+ user = userEvent.setup();
+ });
+
+ it("should integrate with react-hook-form correctly", async () => {
+ const {result} = renderHook(() =>
+ useForm({
+ defaultValues: {
+ defaultValue: "1234",
+ withoutDefaultValue: "",
+ requiredField: "",
+ },
+ }),
+ );
+
+ const {
+ handleSubmit,
+ register,
+ formState: {errors},
+ } = result.current;
+ const onSubmit = jest.fn();
+
+ render(
+ ,
+ );
+
+ await user.click(screen.getByText(/Submit/i));
+ expect(onSubmit).toHaveBeenCalledTimes(0);
+
+ const inputOtp3 = screen.getAllByRole("textbox")[2];
+
+ await user.type(inputOtp3, "1234");
+ await user.click(screen.getByText(/Submit/i));
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/components/input-otp/package.json b/packages/components/input-otp/package.json
new file mode 100644
index 0000000000..0008d78683
--- /dev/null
+++ b/packages/components/input-otp/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "@nextui-org/input-otp",
+ "version": "2.0.0",
+ "description": "",
+ "keywords": [
+ "input-otp"
+ ],
+ "author": "Junior Garcia ",
+ "homepage": "https://nextui.org",
+ "license": "MIT",
+ "main": "src/index.ts",
+ "sideEffects": false,
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/nextui-org/nextui.git",
+ "directory": "packages/components/input-otp"
+ },
+ "bugs": {
+ "url": "https://github.com/nextui-org/nextui/issues"
+ },
+ "scripts": {
+ "build": "tsup src --dts",
+ "build:fast": "tsup src",
+ "dev": "pnpm build:fast --watch",
+ "clean": "rimraf dist .turbo",
+ "typecheck": "tsc --noEmit",
+ "prepack": "clean-package",
+ "postpack": "clean-package restore"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18",
+ "@nextui-org/theme": ">=2.0.0",
+ "@nextui-org/system": ">=2.0.0"
+ },
+ "dependencies": {
+ "@nextui-org/shared-utils": "workspace:*",
+ "@nextui-org/react-utils": "workspace:*",
+ "@react-aria/utils": "3.24.1",
+ "@react-aria/form": "3.0.8",
+ "@react-stately/utils": "3.10.1",
+ "@react-stately/form": "3.0.5",
+ "@react-types/textfield": "3.9.3",
+ "input-otp": "1.4.1"
+ },
+ "devDependencies": {
+ "@nextui-org/theme": "workspace:*",
+ "@nextui-org/system": "workspace:*",
+ "clean-package": "2.2.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-hook-form": "^7.51.3"
+ },
+ "clean-package": "../../../clean-package.config.json"
+}
diff --git a/packages/components/input-otp/src/index.ts b/packages/components/input-otp/src/index.ts
new file mode 100644
index 0000000000..3d5502027c
--- /dev/null
+++ b/packages/components/input-otp/src/index.ts
@@ -0,0 +1,10 @@
+import InputOtp from "./input-otp";
+
+// export types
+export type {InputOtpProps} from "./input-otp";
+
+// export hooks
+export {useInputOtp} from "./use-input-otp";
+
+// export component
+export {InputOtp};
diff --git a/packages/components/input-otp/src/input-otp-context.ts b/packages/components/input-otp/src/input-otp-context.ts
new file mode 100644
index 0000000000..18779ac3d4
--- /dev/null
+++ b/packages/components/input-otp/src/input-otp-context.ts
@@ -0,0 +1,9 @@
+import {createContext} from "@nextui-org/react-utils";
+
+import {UseInputOtpReturn} from "./use-input-otp";
+
+export const [InputOtpProvider, useInputOtpContext] = createContext({
+ name: "InputOtpContext",
+ errorMessage:
+ "useInputOtpContext: `context` is undefined. Seems like you forgot to wrap all input-otp components within ``",
+});
diff --git a/packages/components/input-otp/src/input-otp-segment.tsx b/packages/components/input-otp/src/input-otp-segment.tsx
new file mode 100644
index 0000000000..9a106f63b4
--- /dev/null
+++ b/packages/components/input-otp/src/input-otp-segment.tsx
@@ -0,0 +1,40 @@
+import {SlotProps} from "input-otp";
+import {useMemo} from "react";
+import {clsx, dataAttr} from "@nextui-org/shared-utils";
+
+import {useInputOtpContext} from "./input-otp-context";
+
+export const InputOtpSegment = (props: SlotProps) => {
+ const {classNames, slots, type} = useInputOtpContext();
+
+ const passwordCharStyles = clsx(classNames?.passwordChar);
+ const caretStyles = clsx(classNames?.caret);
+ const segmentStyles = clsx(classNames?.segment);
+
+ const displayValue = useMemo(() => {
+ if (props.isActive && !props.char) {
+ return ;
+ }
+ if (props.char) {
+ return type === "password" ? (
+
+ ) : (
+ {props.char}
+ );
+ }
+
+ return {props.placeholderChar}
;
+ }, [props.char, props.isActive, type]);
+
+ return (
+
+ {displayValue}
+
+ );
+};
diff --git a/packages/components/input-otp/src/input-otp.tsx b/packages/components/input-otp/src/input-otp.tsx
new file mode 100644
index 0000000000..8e8f0cf351
--- /dev/null
+++ b/packages/components/input-otp/src/input-otp.tsx
@@ -0,0 +1,84 @@
+import {forwardRef} from "@nextui-org/system";
+import {useMemo} from "react";
+import {OTPInput} from "input-otp";
+import {clsx} from "@nextui-org/shared-utils";
+
+import {UseInputOtpProps, useInputOtp} from "./use-input-otp";
+import {InputOtpProvider} from "./input-otp-context";
+import {InputOtpSegment} from "./input-otp-segment";
+
+export interface InputOtpProps extends UseInputOtpProps {}
+
+const InputOtp = forwardRef<"div", InputOtpProps>((props, ref) => {
+ const context = useInputOtp({...props, ref});
+
+ const {
+ Component,
+ length,
+ hasHelper,
+ isInvalid,
+ errorMessage,
+ description,
+ slots,
+ classNames,
+ getBaseProps,
+ getInputOtpProps,
+ getSegmentWrapperProps,
+ getHelperWrapperProps,
+ getErrorMessageProps,
+ getDescriptionProps,
+ } = context;
+
+ const helperSection = useMemo(() => {
+ if (!hasHelper) {
+ return null;
+ }
+
+ return (
+
+ {isInvalid && errorMessage ? (
+
{errorMessage}
+ ) : (
+
{description}
+ )}
+
+ );
+ }, [
+ hasHelper,
+ isInvalid,
+ errorMessage,
+ description,
+ getHelperWrapperProps,
+ getErrorMessageProps,
+ getDescriptionProps,
+ ]);
+
+ const wrapperStyles = clsx(classNames?.wrapper);
+
+ return (
+
+
+ (
+
+ {slots.map((slot, idx) => (
+
+ ))}
+
+ )}
+ {...getInputOtpProps()}
+ data-slot="input"
+ role="textbox"
+ />
+ {helperSection}
+
+
+ );
+});
+
+InputOtp.displayName = "NextUI.InputOtp";
+
+export default InputOtp;
diff --git a/packages/components/input-otp/src/use-input-otp.ts b/packages/components/input-otp/src/use-input-otp.ts
new file mode 100644
index 0000000000..f437d67622
--- /dev/null
+++ b/packages/components/input-otp/src/use-input-otp.ts
@@ -0,0 +1,284 @@
+import type {
+ InputOtpReturnType,
+ InputOtpSlots,
+ InputOtpVariantProps,
+ SlotsToClasses,
+} from "@nextui-org/theme";
+
+import {
+ HTMLNextUIProps,
+ mapPropsVariants,
+ PropGetter,
+ useProviderContext,
+} from "@nextui-org/system";
+import {inputOtp} from "@nextui-org/theme";
+import {ReactRef, useDOMRef} from "@nextui-org/react-utils";
+import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils";
+import {useCallback, useMemo} from "react";
+import {chain, mergeProps} from "@react-aria/utils";
+import {AriaTextFieldProps} from "@react-types/textfield";
+import {useControlledState} from "@react-stately/utils";
+import {useFormValidationState} from "@react-stately/form";
+import {useFormValidation} from "@react-aria/form";
+
+interface Props extends HTMLNextUIProps<"div"> {
+ /**
+ * Ref to the DOM node.
+ */
+ ref?: ReactRef;
+ /**
+ * Ref to the container DOM node.
+ */
+ baseRef?: ReactRef;
+ /**
+ * Length required for the otp.
+ */
+ length: number;
+ /**
+ * Regex string for the allowed keys.
+ */
+ allowedKeys?: string;
+ /**
+ * Callback that will be fired when the value has length equal to otp length
+ */
+ onComplete?: (v?: string) => void;
+ /**
+ * Boolean to disable the input-otp component.
+ */
+ isDisabled?: boolean;
+ /**
+ * Boolean to disable the animation in input-otp component.
+ */
+ disableAnimation?: boolean;
+ /**
+ * Classname or List of classes to change the classNames of the element.
+ * if `className` is passed, it will be added to the base slot.
+ *
+ * @example
+ * ```ts
+ *
+ * ```
+ */
+ classNames?: SlotsToClasses;
+ /**
+ * React aria onChange event.
+ */
+ onValueChange?: (value: string) => void;
+}
+
+export type ValueTypes = {
+ slots: InputOtpReturnType;
+ classNames: SlotsToClasses;
+};
+
+export type UseInputOtpProps = Props & InputOtpVariantProps & Omit;
+
+export function useInputOtp(originalProps: UseInputOtpProps) {
+ const globalContext = useProviderContext();
+ const [props, variantProps] = mapPropsVariants(originalProps, inputOtp.variantKeys);
+
+ const {
+ ref,
+ baseRef,
+ as,
+ className,
+ classNames,
+ length = 4,
+ onComplete = () => {},
+ onValueChange = () => {},
+ allowedKeys = "^[0-9]*$",
+ validationBehavior = globalContext?.validationBehavior ?? "aria",
+ type,
+ name,
+ } = props;
+
+ const Component = as || "div";
+
+ const inputRef = useDOMRef(ref);
+ const baseDomRef = useDOMRef(baseRef);
+
+ const handleValueChange = useCallback(
+ (value: string | undefined) => {
+ onValueChange(value ?? "");
+ },
+ [onValueChange],
+ );
+
+ const [value, setValue] = useControlledState(
+ props.value,
+ props.defaultValue ?? "",
+ handleValueChange,
+ );
+
+ const disableAnimation =
+ originalProps.disableAnimation ?? globalContext?.disableAnimation ?? false;
+ const isDisabled = originalProps.isDisabled;
+ const baseStyles = clsx(classNames?.base, className);
+
+ const validationState = useFormValidationState({
+ ...props,
+ validationBehavior,
+ value,
+ });
+
+ useFormValidation(props, validationState, inputRef);
+
+ const {
+ isInvalid: isAriaInvalid,
+ validationErrors,
+ validationDetails,
+ } = validationState.displayValidation;
+ const isReadOnly = originalProps.isReadOnly;
+ const isRequired = originalProps.isRequired;
+ const isInvalid = originalProps.isInvalid || isAriaInvalid;
+ const errorMessage =
+ typeof props.errorMessage === "function"
+ ? props.errorMessage({isInvalid, validationErrors, validationDetails})
+ : props.errorMessage || validationErrors?.join(" ");
+
+ const description = props.description;
+ const hasHelper = !!description || !!errorMessage;
+
+ const slots = useMemo(
+ () =>
+ inputOtp({
+ ...variantProps,
+ disableAnimation,
+ isInvalid,
+ isReadOnly,
+ }),
+ [objectToDeps(variantProps), disableAnimation, isInvalid],
+ );
+
+ const getBaseProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ ref: baseDomRef,
+ className: slots.base({
+ class: baseStyles,
+ }),
+ "data-slot": "base",
+ "data-disabled": dataAttr(isDisabled),
+ "data-invalid": dataAttr(isInvalid),
+ "data-required": dataAttr(originalProps?.isRequired),
+ "data-readonly": dataAttr(originalProps?.isReadOnly),
+ "data-filled": dataAttr(value.length === length),
+ role: "base",
+ ...props,
+ };
+ },
+ [baseDomRef, slots, baseStyles, isDisabled],
+ );
+
+ const getInputOtpProps = useCallback(
+ () => ({
+ required: isRequired,
+ disabled: isDisabled,
+ readOnly: isReadOnly,
+ pattern: allowedKeys,
+ ref: inputRef,
+ name: name,
+ value: value,
+ onChange: chain(setValue),
+ onBlur: props.onBlur,
+ onComplete: onComplete,
+ }),
+ [
+ isRequired,
+ isDisabled,
+ isReadOnly,
+ allowedKeys,
+ inputRef,
+ name,
+ length,
+ props.onChange,
+ setValue,
+ props.onBlur,
+ onComplete,
+ ],
+ );
+
+ const getSegmentWrapperProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ className: slots.segmentWrapper({
+ class: clsx(classNames?.segmentWrapper, props?.className),
+ }),
+ "data-slot": "segment-wrapper",
+ "data-disabled": dataAttr(isDisabled),
+ ...props,
+ };
+ },
+ [slots, classNames?.segmentWrapper, isDisabled],
+ );
+
+ const getHelperWrapperProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ className: slots.helperWrapper({
+ class: clsx(classNames?.helperWrapper, props?.className),
+ }),
+ "data-slot": "helper-wrapper",
+ ...props,
+ };
+ },
+ [slots, classNames?.helperWrapper],
+ );
+
+ const getErrorMessageProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ className: slots.errorMessage({
+ class: clsx(classNames?.errorMessage, props?.className),
+ }),
+ "data-slot": "error-message",
+ ...mergeProps(props),
+ };
+ },
+ [slots, classNames?.errorMessage],
+ );
+
+ const getDescriptionProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ className: slots.description({
+ class: clsx(classNames?.description, props?.className),
+ }),
+ "data-slot": "description",
+ ...mergeProps(props),
+ };
+ },
+ [slots, classNames?.description],
+ );
+
+ return {
+ Component,
+ inputRef,
+ length,
+ value,
+ classNames,
+ slots,
+ hasHelper,
+ isInvalid,
+ description,
+ errorMessage,
+ type,
+ getBaseProps,
+ getInputOtpProps,
+ getSegmentWrapperProps,
+ getHelperWrapperProps,
+ getErrorMessageProps,
+ getDescriptionProps,
+ };
+}
+
+export type UseInputOtpReturn = ReturnType;
diff --git a/packages/components/input-otp/stories/input-otp.stories.tsx b/packages/components/input-otp/stories/input-otp.stories.tsx
new file mode 100644
index 0000000000..9eedd6669d
--- /dev/null
+++ b/packages/components/input-otp/stories/input-otp.stories.tsx
@@ -0,0 +1,271 @@
+import React from "react";
+import {Meta} from "@storybook/react";
+import {button, inputOtp} from "@nextui-org/theme";
+import {useForm} from "react-hook-form";
+import {ValidationResult} from "@react-types/shared";
+
+import {InputOtp} from "../src";
+
+export default {
+ title: "Components/InputOtp",
+ component: InputOtp,
+ argTypes: {
+ variant: {
+ control: {type: "select"},
+ options: ["flat", "faded", "bordered", "underlined"],
+ },
+ color: {
+ control: {type: "select"},
+ options: ["default", "primary", "secondary", "success", "warning", "danger"],
+ },
+ radius: {
+ control: {type: "select"},
+ options: ["none", "sm", "md", "lg", "full"],
+ },
+ size: {
+ control: {type: "select"},
+ options: ["sm", "md", "lg"],
+ },
+ isDisabled: {
+ control: {
+ type: "boolean",
+ },
+ },
+ validationBehavior: {
+ control: {
+ type: "select",
+ },
+ options: ["aria", "native"],
+ },
+ },
+} as Meta;
+
+const defaultProps = {
+ ...inputOtp.defaultVariants,
+};
+
+const Template = (args) => (
+
+
+
+);
+
+const RequiredTemplate = (args) => {
+ const {
+ register,
+ formState: {errors},
+ handleSubmit,
+ } = useForm({
+ defaultValues: {
+ otp: "",
+ },
+ });
+
+ const onSubmit = (data: any) => {
+ // eslint-disable-next-line no-console
+ alert("Submitted value: " + JSON.stringify(data));
+ };
+
+ return (
+
+ );
+};
+
+const ErrorMessageFunctionTemplate = (args) => {
+ const {register, handleSubmit} = useForm({
+ defaultValues: {
+ otp: "",
+ },
+ });
+
+ const onSubmit = (data: any) => {
+ // eslint-disable-next-line no-console
+ alert("Submitted value: " + JSON.stringify(data));
+ };
+
+ return (
+
+
+
+ );
+};
+
+const ControlledTemplate = (args) => {
+ const [value, setValue] = React.useState("");
+
+ return (
+
+
+
Input value: {value}
+
+ );
+};
+
+const WithReactHookFormTemplate = (args) => {
+ const {
+ register,
+ formState: {errors},
+ handleSubmit,
+ } = useForm({
+ defaultValues: {
+ withDefaultValue: "12",
+ requiredField: "",
+ },
+ });
+
+ const onSubmit = (data: any) => {
+ // eslint-disable-next-line no-console
+ console.log(data);
+ alert("Submitted value: " + JSON.stringify(data));
+ };
+
+ return (
+
+ );
+};
+
+export const Default = {
+ render: Template,
+ args: {
+ ...defaultProps,
+ length: 4,
+ },
+};
+
+export const Required = {
+ render: RequiredTemplate,
+ args: {
+ ...defaultProps,
+ length: 4,
+ },
+};
+
+export const Disabled = {
+ render: Template,
+ args: {
+ ...defaultProps,
+ length: 4,
+ defaultValue: "123",
+ isDisabled: true,
+ },
+};
+
+export const ReadOnly = {
+ render: Template,
+ args: {
+ ...defaultProps,
+ length: 4,
+ value: "12",
+ isReadOnly: true,
+ },
+};
+
+export const WithDescription = {
+ render: Template,
+ args: {
+ ...defaultProps,
+ length: 4,
+ description: "description for the otp component",
+ },
+};
+
+export const WithErrorMessage = {
+ render: Template,
+ args: {
+ ...defaultProps,
+ length: 4,
+ isInvalid: true,
+ errorMessage: "Please enter a valid OTP.",
+ },
+};
+
+export const WithErrorMessageFunction = {
+ render: ErrorMessageFunctionTemplate,
+ args: {
+ ...defaultProps,
+ length: 4,
+ isRequired: true,
+ minLength: 4,
+ errorMessage: (value: ValidationResult) => {
+ if (value.validationDetails.tooShort) {
+ return "Value is too short";
+ }
+ },
+ },
+};
+
+export const Password = {
+ render: RequiredTemplate,
+ args: {
+ ...defaultProps,
+ type: "password",
+ },
+};
+
+export const isInvalid = {
+ render: Template,
+ args: {
+ ...defaultProps,
+ length: 4,
+ isInvalid: true,
+ errorMessage: "Invalid OTP",
+ },
+};
+
+export const Controlled = {
+ render: ControlledTemplate,
+ args: {
+ ...defaultProps,
+ },
+};
+
+export const WithReactHookForm = {
+ render: WithReactHookFormTemplate,
+ args: {
+ ...defaultProps,
+ length: 4,
+ },
+};
+
+export const CustomWithClassNames = {
+ render: Template,
+
+ args: {
+ ...defaultProps,
+ length: 4,
+ classNames: {
+ segment: "bg-gradient-to-tr from-pink-500 to-yellow-500",
+ caret: "bg-red-700",
+ },
+ radius: "md",
+ description: "custom otp component",
+ },
+};
diff --git a/packages/components/input-otp/tsconfig.json b/packages/components/input-otp/tsconfig.json
new file mode 100644
index 0000000000..5d012f6e61
--- /dev/null
+++ b/packages/components/input-otp/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "tailwind-variants": ["../../../node_modules/tailwind-variants"]
+ },
+ },
+ "include": ["src", "index.ts"]
+}
diff --git a/packages/components/input-otp/tsup.config.ts b/packages/components/input-otp/tsup.config.ts
new file mode 100644
index 0000000000..3e2bcff6cc
--- /dev/null
+++ b/packages/components/input-otp/tsup.config.ts
@@ -0,0 +1,8 @@
+import {defineConfig} from "tsup";
+
+export default defineConfig({
+ clean: true,
+ target: "es2019",
+ format: ["cjs", "esm"],
+ banner: {js: '"use client";'},
+});
diff --git a/packages/core/react/package.json b/packages/core/react/package.json
index 5513fa10f0..9d2cc0b0d0 100644
--- a/packages/core/react/package.json
+++ b/packages/core/react/package.json
@@ -62,6 +62,7 @@
"@nextui-org/user": "workspace:*",
"@nextui-org/progress": "workspace:*",
"@nextui-org/input": "workspace:*",
+ "@nextui-org/input-otp": "workspace:*",
"@nextui-org/popover": "workspace:*",
"@nextui-org/dropdown": "workspace:*",
"@nextui-org/image": "workspace:*",
diff --git a/packages/core/react/src/index.ts b/packages/core/react/src/index.ts
index cdaab4d346..038933be97 100644
--- a/packages/core/react/src/index.ts
+++ b/packages/core/react/src/index.ts
@@ -45,6 +45,7 @@ export * from "@nextui-org/date-input";
export * from "@nextui-org/date-picker";
export * from "@nextui-org/alert";
export * from "@nextui-org/drawer";
+export * from "@nextui-org/input-otp";
/**
* React Aria - Exports
diff --git a/packages/core/theme/src/components/index.ts b/packages/core/theme/src/components/index.ts
index bd7aa182b2..20be0fb2db 100644
--- a/packages/core/theme/src/components/index.ts
+++ b/packages/core/theme/src/components/index.ts
@@ -17,6 +17,7 @@ export * from "./toggle";
export * from "./accordion";
export * from "./progress";
export * from "./circular-progress";
+export * from "./input-otp";
export * from "./input";
export * from "./dropdown";
export * from "./image";
diff --git a/packages/core/theme/src/components/input-otp.ts b/packages/core/theme/src/components/input-otp.ts
new file mode 100644
index 0000000000..db359587e3
--- /dev/null
+++ b/packages/core/theme/src/components/input-otp.ts
@@ -0,0 +1,458 @@
+import type {VariantProps} from "tailwind-variants";
+
+import {tv} from "../utils/tv";
+
+const inputOtp = tv({
+ slots: {
+ base: ["relative", "flex", "flex-col", "w-fit"],
+ wrapper: ["group", "flex items-center", "has-[:disabled]:opacity-60"],
+ input: [
+ "absolute",
+ "inset-0",
+ "border-none",
+ "outline-none",
+ "bg-transparent",
+ "text-transparent",
+ ],
+ segmentWrapper: ["inline-flex", "gap-x-1", "py-2"],
+ segment: [
+ "h-10",
+ "w-10",
+ "font-semibold",
+ "flex",
+ "justify-center",
+ "items-center",
+ "border-default-200",
+ "data-[active=true]:border-default-400",
+ "data-[active=true]:scale-110",
+ ],
+ passwordChar: ["w-1", "h-1", "bg-default-800", "rounded-full"],
+ caret: [
+ "animate-[appearance-in_1s_infinite]",
+ "font-extralight",
+ "h-full",
+ "w-full",
+ "flex",
+ "justify-center",
+ "items-center",
+ "text-2xl",
+ "h-[50%]",
+ "w-px",
+ "bg-foreground",
+ ],
+ helperWrapper: ["text-xs", "mt-0.5", "font-extralight", ""],
+ errorMessage: ["text-xs text-danger w-full"],
+ description: ["text-xs text-foreground-400"],
+ },
+ variants: {
+ variant: {
+ flat: {
+ segment: ["border-none", "bg-default-100", "data-[active=true]:bg-default-200"],
+ },
+ faded: {
+ segment: ["bg-default-100", "border-1", "data-[active=true]:border-2"],
+ },
+ bordered: {
+ segment: ["border-1", "data-[active=true]:border-2"],
+ },
+ underlined: {
+ segment: ["border-b-1", "data-[active=true]:border-b-2", "data-[active=true]:scale-100"],
+ },
+ },
+ disableAnimation: {
+ true: {
+ segment: "transition-none",
+ },
+ false: {
+ segment: "transition duration-150",
+ },
+ },
+ isDisabled: {
+ true: {
+ segment: "opacity-disabled pointer-events-none",
+ input: "pointer-events-none",
+ },
+ },
+ isInvalid: {
+ true: {},
+ },
+ isReadOnly: {
+ true: {
+ caret: "bg-transparent",
+ segment: "transition-none data-[active=true]:scale-100",
+ },
+ },
+ fullWidth: {
+ true: {
+ base: "w-full",
+ },
+ },
+ radius: {
+ none: {
+ segment: "rounded-none",
+ },
+ sm: {
+ segment: "rounded-sm",
+ },
+ md: {
+ segment: "rounded-md",
+ },
+ lg: {
+ segment: "rounded-lg",
+ },
+ full: {
+ segment: "rounded-full",
+ },
+ },
+ color: {
+ default: {},
+ primary: {},
+ secondary: {},
+ success: {},
+ warning: {},
+ danger: {},
+ },
+ size: {
+ sm: {
+ segment: "h-8 min-h-8 w-8 min-w-8 text-small",
+ },
+ md: {
+ segment: "h-10 min-h-10 w-10 min-w-10 text-small",
+ },
+ lg: {
+ segment: "h-12 min-h-12 w-12 min-w-12 text-medium",
+ },
+ },
+ },
+ defaultVariants: {
+ variant: "flat",
+ color: "default",
+ },
+ compoundVariants: [
+ // flat & color
+ {
+ variant: "flat",
+ color: "default",
+ class: {
+ segment: ["bg-default-200", "data-[active=true]:bg-default-400"],
+ },
+ },
+ {
+ variant: "flat",
+ color: "primary",
+ class: {
+ segment: ["bg-primary-100", "data-[active=true]:bg-primary-200", "text-primary"],
+ caret: ["bg-primary"],
+ passwordChar: ["bg-primary"],
+ },
+ },
+ {
+ variant: "flat",
+ color: "secondary",
+ class: {
+ segment: ["bg-secondary-100", "data-[active=true]:bg-secondary-200", "text-secondary"],
+ caret: ["bg-secondary"],
+ passwordChar: ["bg-secondary"],
+ },
+ },
+ {
+ variant: "flat",
+ color: "success",
+ class: {
+ segment: ["bg-success-100", "data-[active=true]:bg-success-200", "text-success"],
+ caret: ["bg-success"],
+ passwordChar: ["bg-success"],
+ },
+ },
+ {
+ variant: "flat",
+ color: "warning",
+ class: {
+ segment: ["bg-warning-100", "data-[active=true]:bg-warning-200", "text-warning"],
+ caret: ["bg-warning"],
+ passwordChar: ["bg-warning"],
+ },
+ },
+ {
+ variant: "flat",
+ color: "danger",
+ class: {
+ segment: ["bg-danger-100", "data-[active=true]:bg-danger-200", "text-danger"],
+ caret: ["bg-danger"],
+ passwordChar: ["bg-danger"],
+ },
+ },
+ // faded & color
+ {
+ variant: "faded",
+ color: "default",
+ class: {
+ segment: "bg-default-200",
+ },
+ },
+ {
+ variant: "faded",
+ color: "primary",
+ class: {
+ segment: [
+ "bg-primary-100",
+ "text-primary",
+ "border-1",
+ "border-primary-200",
+ "data-[active=true]:border-2",
+ "data-[active=true]:border-primary-400",
+ ],
+ caret: ["bg-primary"],
+ passwordChar: ["bg-primary"],
+ },
+ },
+ {
+ variant: "faded",
+ color: "secondary",
+ class: {
+ segment: [
+ "bg-secondary-100",
+ "text-secondary",
+ "border-1",
+ "border-secondary-200",
+ "data-[active=true]:border-2",
+ "data-[active=true]:border-secondary-400",
+ ],
+ caret: ["bg-secondary"],
+ passwordChar: ["bg-secondary"],
+ },
+ },
+ {
+ variant: "faded",
+ color: "success",
+ class: {
+ segment: [
+ "bg-success-100",
+ "text-success",
+ "border-1",
+ "border-success-200",
+ "data-[active=true]:border-2",
+ "data-[active=true]:border-success-400",
+ ],
+ caret: ["bg-success"],
+ passwordChar: ["bg-success"],
+ },
+ },
+ {
+ variant: "faded",
+ color: "warning",
+ class: {
+ segment: [
+ "bg-warning-100",
+ "text-warning",
+ "border-1",
+ "border-warning-200",
+ "data-[active=true]:border-2",
+ "data-[active=true]:border-warning-400",
+ ],
+ caret: ["bg-warning"],
+ passwordChar: ["bg-warning"],
+ },
+ },
+ {
+ variant: "faded",
+ color: "danger",
+ class: {
+ segment: [
+ "bg-danger-100",
+ "text-danger",
+ "border-1",
+ "border-danger-200",
+ "data-[active=true]:border-2",
+ "data-[active=true]:border-danger-400",
+ ],
+ caret: ["bg-danger"],
+ passwordChar: ["bg-danger"],
+ },
+ },
+ // bordered & color
+ {
+ variant: "bordered",
+ color: "default",
+ class: {
+ segment: "data-[has-value=true]:text-default-foreground",
+ },
+ },
+ {
+ variant: "bordered",
+ color: "primary",
+ class: {
+ segment: ["border-primary-200", "text-primary", "data-[active=true]:border-primary-400"],
+ caret: ["bg-primary"],
+ passwordChar: ["bg-primary"],
+ },
+ },
+ {
+ variant: "bordered",
+ color: "secondary",
+ class: {
+ segment: [
+ "border-secondary-200",
+ "text-secondary",
+ "data-[active=true]:border-secondary-400",
+ ],
+ caret: ["bg-secondary"],
+ passwordChar: ["bg-secondary"],
+ },
+ },
+ {
+ variant: "bordered",
+ color: "success",
+ class: {
+ segment: ["border-success-200", "text-success", "data-[active=true]:border-success-400"],
+ caret: ["bg-success"],
+ passwordChar: ["bg-success"],
+ },
+ },
+ {
+ variant: "bordered",
+ color: "warning",
+ class: {
+ segment: ["border-warning-200", "text-warning", "data-[active=true]:border-warning-400"],
+ caret: ["bg-warning"],
+ passwordChar: ["bg-warning"],
+ },
+ },
+ {
+ variant: "bordered",
+ color: "danger",
+ class: {
+ segment: ["border-danger-200", "text-danger", "data-[active=true]:border-danger-400"],
+ caret: ["bg-danger"],
+ passwordChar: ["bg-danger"],
+ },
+ },
+ // underlined & color
+ {
+ variant: "underlined",
+ color: "default",
+ class: {
+ segment: "data-[has-value=true]:text-default-foreground rounded-none",
+ },
+ },
+ {
+ variant: "underlined",
+ color: "primary",
+ class: {
+ segment: [
+ "border-primary-200",
+ "text-primary",
+ "data-[active=true]:border-primary-400",
+ "rounded-none",
+ ],
+ caret: ["bg-primary"],
+ passwordChar: ["bg-primary"],
+ },
+ },
+ {
+ variant: "underlined",
+ color: "secondary",
+ class: {
+ segment: [
+ "border-secondary-200",
+ "text-secondary",
+ "data-[active=true]:border-secondary-400",
+ "rounded-none",
+ ],
+ caret: ["bg-secondary"],
+ passwordChar: ["bg-secondary"],
+ },
+ },
+ {
+ variant: "underlined",
+ color: "success",
+ class: {
+ segment: [
+ "border-success-200",
+ "text-success",
+ "data-[active=true]:border-success-400",
+ "rounded-none",
+ ],
+ caret: ["bg-success"],
+ passwordChar: ["bg-success"],
+ },
+ },
+ {
+ variant: "underlined",
+ color: "warning",
+ class: {
+ segment: [
+ "border-warning-200",
+ "text-warning",
+ "data-[active=true]:border-warning-400",
+ "rounded-none",
+ ],
+ caret: ["bg-warning"],
+ passwordChar: ["bg-warning"],
+ },
+ },
+ {
+ variant: "underlined",
+ color: "danger",
+ class: {
+ segment: [
+ "border-danger-200",
+ "text-danger",
+ "data-[active=true]:border-danger-400",
+ "rounded-none",
+ ],
+ caret: ["bg-danger"],
+ passwordChar: ["bg-danger"],
+ },
+ },
+ // isInvalid and flat
+ {
+ variant: "flat",
+ isInvalid: true,
+ class: {
+ segment: ["bg-danger-50", "data-[active=true]:bg-danger-100", "text-danger"],
+ caret: ["bg-danger"],
+ },
+ },
+ // isInvalid and faded
+ {
+ variant: "faded",
+ isInvalid: true,
+ class: {
+ segment: [
+ "bg-danger-50",
+ "text-danger",
+ "border-1",
+ "border-danger-200",
+ "data-[active=true]:border-2",
+ "data-[active=true]:border-danger-400",
+ ],
+ caret: ["bg-danger"],
+ },
+ },
+ // isInvalid and bordered
+ {
+ variant: "bordered",
+ isInvalid: true,
+ class: {
+ segment: ["border-danger-200", "text-danger", "data-[active=true]:border-danger-400"],
+ caret: ["bg-danger"],
+ },
+ },
+ // isInvalid anf underlined
+ {
+ variant: "underlined",
+ isInvalid: true,
+ class: {
+ segment: ["border-danger-200", "text-danger", "data-[active=true]:border-danger-400"],
+ caret: ["bg-danger"],
+ },
+ },
+ ],
+});
+
+export type InputOtpVariantProps = VariantProps;
+export type InputOtpSlots = keyof ReturnType;
+export type InputOtpReturnType = ReturnType;
+
+export {inputOtp};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 25fe5137e6..226b775b01 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -291,6 +291,9 @@ importers:
'@nextui-org/divider':
specifier: workspace:*
version: link:../../packages/components/divider
+ '@nextui-org/input-otp':
+ specifier: workspace:*
+ version: link:../../packages/components/input-otp
'@nextui-org/kbd':
specifier: workspace:*
version: link:../../packages/components/kbd
@@ -1662,6 +1665,52 @@ importers:
specifier: ^7.51.3
version: 7.53.1(react@18.3.1)
+ packages/components/input-otp:
+ dependencies:
+ '@nextui-org/react-utils':
+ specifier: workspace:*
+ version: link:../../utilities/react-utils
+ '@nextui-org/shared-utils':
+ specifier: workspace:*
+ version: link:../../utilities/shared-utils
+ '@react-aria/form':
+ specifier: 3.0.8
+ version: 3.0.8(react@18.3.1)
+ '@react-aria/utils':
+ specifier: 3.24.1
+ version: 3.24.1(react@18.3.1)
+ '@react-stately/form':
+ specifier: 3.0.5
+ version: 3.0.5(react@18.3.1)
+ '@react-stately/utils':
+ specifier: 3.10.1
+ version: 3.10.1(react@18.3.1)
+ '@react-types/textfield':
+ specifier: 3.9.3
+ version: 3.9.3(react@18.3.1)
+ input-otp:
+ specifier: 1.4.1
+ version: 1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ devDependencies:
+ '@nextui-org/system':
+ specifier: workspace:*
+ version: link:../../core/system
+ '@nextui-org/theme':
+ specifier: workspace:*
+ version: link:../../core/theme
+ clean-package:
+ specifier: 2.2.0
+ version: 2.2.0
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
+ react-hook-form:
+ specifier: ^7.51.3
+ version: 7.53.1(react@18.3.1)
+
packages/components/kbd:
dependencies:
'@nextui-org/react-utils':
@@ -2995,6 +3044,9 @@ importers:
'@nextui-org/input':
specifier: workspace:*
version: link:../../components/input
+ '@nextui-org/input-otp':
+ specifier: workspace:*
+ version: link:../../components/input-otp
'@nextui-org/kbd':
specifier: workspace:*
version: link:../../components/kbd
@@ -7105,6 +7157,11 @@ packages:
peerDependencies:
react: ^18.2.0
+ '@react-types/textfield@3.9.3':
+ resolution: {integrity: sha512-DoAY6cYOL0pJhgNGI1Rosni7g72GAt4OVr2ltEx2S9ARmFZ0DBvdhA9lL2nywcnKMf27PEJcKMXzXc10qaHsJw==}
+ peerDependencies:
+ react: ^18.2.0
+
'@react-types/textfield@3.9.6':
resolution: {integrity: sha512-0uPqjJh4lYp1aL1HL9IlV8Cgp8eT0PcsNfdoCktfkLytvvBPmox2Pfm57W/d0xTtzZu2CjxhYNTob+JtGAOeXA==}
peerDependencies:
@@ -11054,6 +11111,12 @@ packages:
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
+ input-otp@1.4.1:
+ resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==}
+ peerDependencies:
+ react: ^18.2.0
+ react-dom: ^18.2.0
+
inquirer@6.5.2:
resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==}
engines: {node: '>=6.0.0'}
@@ -20334,6 +20397,11 @@ snapshots:
'@react-types/shared': 3.24.1(react@18.3.1)
react: 18.3.1
+ '@react-types/textfield@3.9.3(react@18.3.1)':
+ dependencies:
+ '@react-types/shared': 3.24.1(react@18.3.1)
+ react: 18.3.1
+
'@react-types/textfield@3.9.6(react@18.3.1)':
dependencies:
'@react-types/shared': 3.24.1(react@18.3.1)
@@ -23833,7 +23901,7 @@ snapshots:
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@7.32.0)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@7.32.0)
eslint-plugin-react: 7.37.2(eslint@7.32.0)
eslint-plugin-react-hooks: 4.6.2(eslint@7.32.0)
@@ -23903,7 +23971,7 @@ snapshots:
is-bun-module: 1.2.1
is-glob: 4.0.3
optionalDependencies:
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-node
@@ -23954,35 +24022,6 @@ snapshots:
lodash: 4.17.21
string-natural-compare: 3.0.1
- eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0):
- dependencies:
- '@rtsao/scc': 1.1.0
- array-includes: 3.1.8
- array.prototype.findlastindex: 1.2.5
- array.prototype.flat: 1.3.2
- array.prototype.flatmap: 1.3.2
- debug: 3.2.7
- doctrine: 2.1.0
- eslint: 7.32.0
- eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0)
- hasown: 2.0.2
- is-core-module: 2.15.1
- is-glob: 4.0.3
- minimatch: 3.1.2
- object.fromentries: 2.0.8
- object.groupby: 1.0.3
- object.values: 1.2.0
- semver: 6.3.1
- string.prototype.trimend: 1.0.8
- tsconfig-paths: 3.15.0
- optionalDependencies:
- '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@4.9.5)
- transitivePeerDependencies:
- - eslint-import-resolver-typescript
- - eslint-import-resolver-webpack
- - supports-color
-
eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0):
dependencies:
'@rtsao/scc': 1.1.0
@@ -25351,6 +25390,11 @@ snapshots:
inline-style-parser@0.2.4: {}
+ input-otp@1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
inquirer@6.5.2:
dependencies:
ansi-escapes: 3.2.0