Skip to content

Commit e8856d1

Browse files
authored
feat: add input-otp (shadcn-ui#2919)
* feat: add input-otp * feat: update input-otp and add examples * feat(input-otp): add controlled and form examples * chore(input-otp): update to latest * docs(www): fix example code for input-otp * fix(www): disable menu
1 parent 7f0af43 commit e8856d1

22 files changed

+907
-4
lines changed

apps/www/__registry__/index.tsx

+84
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ export const Index: Record<string, any> = {
152152
component: React.lazy(() => import("@/registry/default/ui/input")),
153153
files: ["registry/default/ui/input.tsx"],
154154
},
155+
"input-otp": {
156+
name: "input-otp",
157+
type: "components:ui",
158+
registryDependencies: undefined,
159+
component: React.lazy(() => import("@/registry/default/ui/input-otp")),
160+
files: ["registry/default/ui/input-otp.tsx"],
161+
},
155162
"label": {
156163
name: "label",
157164
type: "components:ui",
@@ -803,6 +810,41 @@ export const Index: Record<string, any> = {
803810
component: React.lazy(() => import("@/registry/default/example/input-with-text")),
804811
files: ["registry/default/example/input-with-text.tsx"],
805812
},
813+
"input-otp-demo": {
814+
name: "input-otp-demo",
815+
type: "components:example",
816+
registryDependencies: ["input-otp"],
817+
component: React.lazy(() => import("@/registry/default/example/input-otp-demo")),
818+
files: ["registry/default/example/input-otp-demo.tsx"],
819+
},
820+
"input-otp-pattern": {
821+
name: "input-otp-pattern",
822+
type: "components:example",
823+
registryDependencies: ["input-otp"],
824+
component: React.lazy(() => import("@/registry/default/example/input-otp-pattern")),
825+
files: ["registry/default/example/input-otp-pattern.tsx"],
826+
},
827+
"input-otp-separator": {
828+
name: "input-otp-separator",
829+
type: "components:example",
830+
registryDependencies: ["input-otp"],
831+
component: React.lazy(() => import("@/registry/default/example/input-otp-separator")),
832+
files: ["registry/default/example/input-otp-separator.tsx"],
833+
},
834+
"input-otp-controlled": {
835+
name: "input-otp-controlled",
836+
type: "components:example",
837+
registryDependencies: ["input-otp"],
838+
component: React.lazy(() => import("@/registry/default/example/input-otp-controlled")),
839+
files: ["registry/default/example/input-otp-controlled.tsx"],
840+
},
841+
"input-otp-form": {
842+
name: "input-otp-form",
843+
type: "components:example",
844+
registryDependencies: ["input-otp","form"],
845+
component: React.lazy(() => import("@/registry/default/example/input-otp-form")),
846+
files: ["registry/default/example/input-otp-form.tsx"],
847+
},
806848
"label-demo": {
807849
name: "label-demo",
808850
type: "components:example",
@@ -1427,6 +1469,13 @@ export const Index: Record<string, any> = {
14271469
component: React.lazy(() => import("@/registry/new-york/ui/input")),
14281470
files: ["registry/new-york/ui/input.tsx"],
14291471
},
1472+
"input-otp": {
1473+
name: "input-otp",
1474+
type: "components:ui",
1475+
registryDependencies: undefined,
1476+
component: React.lazy(() => import("@/registry/new-york/ui/input-otp")),
1477+
files: ["registry/new-york/ui/input-otp.tsx"],
1478+
},
14301479
"label": {
14311480
name: "label",
14321481
type: "components:ui",
@@ -2078,6 +2127,41 @@ export const Index: Record<string, any> = {
20782127
component: React.lazy(() => import("@/registry/new-york/example/input-with-text")),
20792128
files: ["registry/new-york/example/input-with-text.tsx"],
20802129
},
2130+
"input-otp-demo": {
2131+
name: "input-otp-demo",
2132+
type: "components:example",
2133+
registryDependencies: ["input-otp"],
2134+
component: React.lazy(() => import("@/registry/new-york/example/input-otp-demo")),
2135+
files: ["registry/new-york/example/input-otp-demo.tsx"],
2136+
},
2137+
"input-otp-pattern": {
2138+
name: "input-otp-pattern",
2139+
type: "components:example",
2140+
registryDependencies: ["input-otp"],
2141+
component: React.lazy(() => import("@/registry/new-york/example/input-otp-pattern")),
2142+
files: ["registry/new-york/example/input-otp-pattern.tsx"],
2143+
},
2144+
"input-otp-separator": {
2145+
name: "input-otp-separator",
2146+
type: "components:example",
2147+
registryDependencies: ["input-otp"],
2148+
component: React.lazy(() => import("@/registry/new-york/example/input-otp-separator")),
2149+
files: ["registry/new-york/example/input-otp-separator.tsx"],
2150+
},
2151+
"input-otp-controlled": {
2152+
name: "input-otp-controlled",
2153+
type: "components:example",
2154+
registryDependencies: ["input-otp"],
2155+
component: React.lazy(() => import("@/registry/new-york/example/input-otp-controlled")),
2156+
files: ["registry/new-york/example/input-otp-controlled.tsx"],
2157+
},
2158+
"input-otp-form": {
2159+
name: "input-otp-form",
2160+
type: "components:example",
2161+
registryDependencies: ["input-otp","form"],
2162+
component: React.lazy(() => import("@/registry/new-york/example/input-otp-form")),
2163+
files: ["registry/new-york/example/input-otp-form.tsx"],
2164+
},
20812165
"label-demo": {
20822166
name: "label-demo",
20832167
type: "components:example",

apps/www/config/docs.ts

+6
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ export const docsConfig: DocsConfig = {
209209
href: "/docs/components/input",
210210
items: [],
211211
},
212+
// {
213+
// title: "Input OTP",
214+
// href: "/docs/components/input-otp",
215+
// items: [],
216+
// label: "New",
217+
// },
212218
{
213219
title: "Label",
214220
href: "/docs/components/label",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
---
2+
title: Input OTP
3+
description: Accessible one-time password component with copy paste functionality.
4+
component: true
5+
links:
6+
doc: https://input-otp.rodz.dev
7+
---
8+
9+
<ComponentPreview name="input-otp-demo" />
10+
11+
## About
12+
13+
Input OTP is built on top of [input-otp](https://github.com/guilhermerodz/input-otp) by [@guilherme_rodz](https://twitter.com/guilherme_rodz).
14+
15+
## Installation
16+
17+
<Tabs defaultValue="cli">
18+
19+
<TabsList>
20+
<TabsTrigger value="cli">CLI</TabsTrigger>
21+
<TabsTrigger value="manual">Manual</TabsTrigger>
22+
</TabsList>
23+
<TabsContent value="cli">
24+
25+
<Steps>
26+
27+
<Step>Run the following command:</Step>
28+
29+
```bash
30+
npx shadcn-ui@latest add input-otp
31+
```
32+
33+
<Step>Update `tailwind.config.js`</Step>
34+
35+
Add the following animations to your `tailwind.config.js` file:
36+
37+
```js showLineNumbers title="tailwind.config.js" {6-9,12}
38+
/** @type {import('tailwindcss').Config} */
39+
module.exports = {
40+
theme: {
41+
extend: {
42+
keyframes: {
43+
"caret-blink": {
44+
"0%,70%,100%": { opacity: "1" },
45+
"20%,50%": { opacity: "0" },
46+
},
47+
},
48+
animation: {
49+
"caret-blink": "caret-blink 1.25s ease-out infinite",
50+
},
51+
},
52+
},
53+
}
54+
```
55+
56+
</Steps>
57+
58+
</TabsContent>
59+
60+
<TabsContent value="manual">
61+
62+
<Steps>
63+
64+
<Step>Install the following dependencies:</Step>
65+
66+
```bash
67+
npm install input-otp
68+
```
69+
70+
<Step>Copy and paste the following code into your project.</Step>
71+
72+
<ComponentSource name="input-otp" />
73+
74+
<Step>Update the import paths to match your project setup.</Step>
75+
76+
<Step>Update `tailwind.config.js`</Step>
77+
78+
Add the following animations to your `tailwind.config.js` file:
79+
80+
```js showLineNumbers title="tailwind.config.js" {6-9,12}
81+
/** @type {import('tailwindcss').Config} */
82+
module.exports = {
83+
theme: {
84+
extend: {
85+
keyframes: {
86+
"caret-blink": {
87+
"0%,70%,100%": { opacity: "1" },
88+
"20%,50%": { opacity: "0" },
89+
},
90+
},
91+
animation: {
92+
"caret-blink": "caret-blink 1.25s ease-out infinite",
93+
},
94+
},
95+
},
96+
}
97+
```
98+
99+
</Steps>
100+
101+
</TabsContent>
102+
103+
</Tabs>
104+
105+
## Usage
106+
107+
```tsx
108+
import {
109+
InputOTP,
110+
InputOTPGroup,
111+
InputOTPSeparator,
112+
InputOTPSlot,
113+
} from "@/components/ui/input-otp"
114+
```
115+
116+
```tsx
117+
<InputOTP
118+
maxLength={6}
119+
render={({ slots }) => (
120+
<>
121+
<InputOTPGroup>
122+
{slots.slice(0, 3).map((slot, index) => (
123+
<InputOTPSlot key={index} {...slot} />
124+
))}{" "}
125+
</InputOTPGroup>
126+
<InputOTPSeparator />
127+
<InputOTPGroup>
128+
{slots.slice(3).map((slot, index) => (
129+
<InputOTPSlot key={index} {...slot} />
130+
))}
131+
</InputOTPGroup>
132+
</>
133+
)}
134+
/>
135+
```
136+
137+
## Examples
138+
139+
### Pattern
140+
141+
Use the `pattern` prop to define a custom pattern for the OTP input.
142+
143+
<ComponentPreview name="input-otp-pattern" />
144+
145+
```tsx showLineNumbers {1,7}
146+
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"
147+
148+
...
149+
150+
<InputOTP
151+
maxLength={6}
152+
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
153+
render={({ slots }) => (
154+
<InputOTPGroup>
155+
{slots.map((slot, index) => (
156+
<InputOTPSlot key={index} {...slot} />
157+
))}{" "}
158+
</InputOTPGroup>
159+
)}
160+
/>
161+
```
162+
163+
### Separator
164+
165+
You can use the `<InputOTPSeparator />` component to add a separator between the input groups.
166+
167+
<ComponentPreview name="input-otp-separator" />
168+
169+
```tsx showLineNumbers {4,17}
170+
import {
171+
InputOTP,
172+
InputOTPGroup,
173+
InputOTPSeparator,
174+
InputOTPSlot,
175+
} from "@/registry/new-york/ui/input-otp"
176+
177+
...
178+
179+
<InputOTP
180+
maxLength={6}
181+
render={({ slots }) => (
182+
<InputOTPGroup className="gap-2">
183+
{slots.map((slot, index) => (
184+
<React.Fragment key={index}>
185+
<InputOTPSlot className="rounded-md border" {...slot} />
186+
{index !== slots.length - 1 && <InputOTPSeparator />}
187+
</React.Fragment>
188+
))}{" "}
189+
</InputOTPGroup>
190+
)}
191+
/>
192+
```
193+
194+
### Controlled
195+
196+
You can use the `value` and `onChange` props to control the input value.
197+
198+
<ComponentPreview name="input-otp-controlled" />
199+
200+
### Form
201+
202+
<ComponentPreview name="input-otp-form" />

apps/www/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"embla-carousel-autoplay": "8.0.0-rc15",
5959
"embla-carousel-react": "8.0.0-rc15",
6060
"geist": "^1.1.0",
61+
"input-otp": "^1.0.1",
6162
"jotai": "^2.1.0",
6263
"lodash.template": "^4.5.0",
6364
"lucide-react": "0.288.0",

apps/www/public/registry/index.json

+10
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@
219219
],
220220
"type": "components:ui"
221221
},
222+
{
223+
"name": "input-otp",
224+
"dependencies": [
225+
"input-otp"
226+
],
227+
"files": [
228+
"ui/input-otp.tsx"
229+
],
230+
"type": "components:ui"
231+
},
222232
{
223233
"name": "label",
224234
"dependencies": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "input-otp",
3+
"dependencies": [
4+
"input-otp"
5+
],
6+
"files": [
7+
{
8+
"name": "input-otp.tsx",
9+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { OTPInput, SlotProps } from \"input-otp\"\nimport { Dot } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst InputOTP = React.forwardRef<\n React.ElementRef<typeof OTPInput>,\n React.ComponentPropsWithoutRef<typeof OTPInput>\n>(({ className, ...props }, ref) => (\n <OTPInput\n ref={ref}\n containerClassName={cn(\"flex items-center gap-2\", className)}\n {...props}\n />\n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex items-center\", className)} {...props} />\n))\nInputOTPGroup.displayName = \"InputOTPGroup\"\n\nconst InputOTPSlot = React.forwardRef<\n React.ElementRef<\"div\">,\n SlotProps & React.ComponentPropsWithoutRef<\"div\">\n>(({ char, hasFakeCaret, isActive, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn(\n \"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md\",\n isActive && \"z-10 ring-2 ring-offset-background ring-ring\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink h-4 w-px bg-foreground duration-1000\" />\n </div>\n )}\n </div>\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n <div ref={ref} role=\"separator\" {...props}>\n <Dot />\n </div>\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
10+
}
11+
],
12+
"type": "components:ui"
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "input-otp",
3+
"dependencies": [
4+
"input-otp"
5+
],
6+
"files": [
7+
{
8+
"name": "input-otp.tsx",
9+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { DashIcon } from \"@radix-ui/react-icons\"\nimport { OTPInput, SlotProps } from \"input-otp\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst InputOTP = React.forwardRef<\n React.ElementRef<typeof OTPInput>,\n React.ComponentPropsWithoutRef<typeof OTPInput>\n>(({ className, ...props }, ref) => (\n <OTPInput\n ref={ref}\n containerClassName={cn(\"flex items-center gap-2\", className)}\n {...props}\n />\n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex items-center\", className)} {...props} />\n))\nInputOTPGroup.displayName = \"InputOTPGroup\"\n\nconst InputOTPSlot = React.forwardRef<\n React.ElementRef<\"div\">,\n SlotProps & React.ComponentPropsWithoutRef<\"div\">\n>(({ char, hasFakeCaret, isActive, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn(\n \"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md\",\n isActive && \"z-10 ring-1 ring-ring\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink h-4 w-px bg-foreground duration-1000\" />\n </div>\n )}\n </div>\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n <div ref={ref} role=\"separator\" {...props}>\n <DashIcon />\n </div>\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
10+
}
11+
],
12+
"type": "components:ui"
13+
}

0 commit comments

Comments
 (0)