mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-24 13:15:45 +00:00
feat: add input-otp (#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
This commit is contained in:
@@ -152,6 +152,13 @@ export const Index: Record<string, any> = {
|
||||
component: React.lazy(() => import("@/registry/default/ui/input")),
|
||||
files: ["registry/default/ui/input.tsx"],
|
||||
},
|
||||
"input-otp": {
|
||||
name: "input-otp",
|
||||
type: "components:ui",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/ui/input-otp")),
|
||||
files: ["registry/default/ui/input-otp.tsx"],
|
||||
},
|
||||
"label": {
|
||||
name: "label",
|
||||
type: "components:ui",
|
||||
@@ -803,6 +810,41 @@ export const Index: Record<string, any> = {
|
||||
component: React.lazy(() => import("@/registry/default/example/input-with-text")),
|
||||
files: ["registry/default/example/input-with-text.tsx"],
|
||||
},
|
||||
"input-otp-demo": {
|
||||
name: "input-otp-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
component: React.lazy(() => import("@/registry/default/example/input-otp-demo")),
|
||||
files: ["registry/default/example/input-otp-demo.tsx"],
|
||||
},
|
||||
"input-otp-pattern": {
|
||||
name: "input-otp-pattern",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
component: React.lazy(() => import("@/registry/default/example/input-otp-pattern")),
|
||||
files: ["registry/default/example/input-otp-pattern.tsx"],
|
||||
},
|
||||
"input-otp-separator": {
|
||||
name: "input-otp-separator",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
component: React.lazy(() => import("@/registry/default/example/input-otp-separator")),
|
||||
files: ["registry/default/example/input-otp-separator.tsx"],
|
||||
},
|
||||
"input-otp-controlled": {
|
||||
name: "input-otp-controlled",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
component: React.lazy(() => import("@/registry/default/example/input-otp-controlled")),
|
||||
files: ["registry/default/example/input-otp-controlled.tsx"],
|
||||
},
|
||||
"input-otp-form": {
|
||||
name: "input-otp-form",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp","form"],
|
||||
component: React.lazy(() => import("@/registry/default/example/input-otp-form")),
|
||||
files: ["registry/default/example/input-otp-form.tsx"],
|
||||
},
|
||||
"label-demo": {
|
||||
name: "label-demo",
|
||||
type: "components:example",
|
||||
@@ -1427,6 +1469,13 @@ export const Index: Record<string, any> = {
|
||||
component: React.lazy(() => import("@/registry/new-york/ui/input")),
|
||||
files: ["registry/new-york/ui/input.tsx"],
|
||||
},
|
||||
"input-otp": {
|
||||
name: "input-otp",
|
||||
type: "components:ui",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/new-york/ui/input-otp")),
|
||||
files: ["registry/new-york/ui/input-otp.tsx"],
|
||||
},
|
||||
"label": {
|
||||
name: "label",
|
||||
type: "components:ui",
|
||||
@@ -2078,6 +2127,41 @@ export const Index: Record<string, any> = {
|
||||
component: React.lazy(() => import("@/registry/new-york/example/input-with-text")),
|
||||
files: ["registry/new-york/example/input-with-text.tsx"],
|
||||
},
|
||||
"input-otp-demo": {
|
||||
name: "input-otp-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
component: React.lazy(() => import("@/registry/new-york/example/input-otp-demo")),
|
||||
files: ["registry/new-york/example/input-otp-demo.tsx"],
|
||||
},
|
||||
"input-otp-pattern": {
|
||||
name: "input-otp-pattern",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
component: React.lazy(() => import("@/registry/new-york/example/input-otp-pattern")),
|
||||
files: ["registry/new-york/example/input-otp-pattern.tsx"],
|
||||
},
|
||||
"input-otp-separator": {
|
||||
name: "input-otp-separator",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
component: React.lazy(() => import("@/registry/new-york/example/input-otp-separator")),
|
||||
files: ["registry/new-york/example/input-otp-separator.tsx"],
|
||||
},
|
||||
"input-otp-controlled": {
|
||||
name: "input-otp-controlled",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
component: React.lazy(() => import("@/registry/new-york/example/input-otp-controlled")),
|
||||
files: ["registry/new-york/example/input-otp-controlled.tsx"],
|
||||
},
|
||||
"input-otp-form": {
|
||||
name: "input-otp-form",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp","form"],
|
||||
component: React.lazy(() => import("@/registry/new-york/example/input-otp-form")),
|
||||
files: ["registry/new-york/example/input-otp-form.tsx"],
|
||||
},
|
||||
"label-demo": {
|
||||
name: "label-demo",
|
||||
type: "components:example",
|
||||
|
||||
@@ -209,6 +209,12 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/docs/components/input",
|
||||
items: [],
|
||||
},
|
||||
// {
|
||||
// title: "Input OTP",
|
||||
// href: "/docs/components/input-otp",
|
||||
// items: [],
|
||||
// label: "New",
|
||||
// },
|
||||
{
|
||||
title: "Label",
|
||||
href: "/docs/components/label",
|
||||
|
||||
202
apps/www/content/docs/components/input-otp.mdx
Normal file
202
apps/www/content/docs/components/input-otp.mdx
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: Input OTP
|
||||
description: Accessible one-time password component with copy paste functionality.
|
||||
component: true
|
||||
links:
|
||||
doc: https://input-otp.rodz.dev
|
||||
---
|
||||
|
||||
<ComponentPreview name="input-otp-demo" />
|
||||
|
||||
## About
|
||||
|
||||
Input OTP is built on top of [input-otp](https://github.com/guilhermerodz/input-otp) by [@guilherme_rodz](https://twitter.com/guilherme_rodz).
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs defaultValue="cli">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="cli">CLI</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="cli">
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>Run the following command:</Step>
|
||||
|
||||
```bash
|
||||
npx shadcn-ui@latest add input-otp
|
||||
```
|
||||
|
||||
<Step>Update `tailwind.config.js`</Step>
|
||||
|
||||
Add the following animations to your `tailwind.config.js` file:
|
||||
|
||||
```js showLineNumbers title="tailwind.config.js" {6-9,12}
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
keyframes: {
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual">
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>Install the following dependencies:</Step>
|
||||
|
||||
```bash
|
||||
npm install input-otp
|
||||
```
|
||||
|
||||
<Step>Copy and paste the following code into your project.</Step>
|
||||
|
||||
<ComponentSource name="input-otp" />
|
||||
|
||||
<Step>Update the import paths to match your project setup.</Step>
|
||||
|
||||
<Step>Update `tailwind.config.js`</Step>
|
||||
|
||||
Add the following animations to your `tailwind.config.js` file:
|
||||
|
||||
```js showLineNumbers title="tailwind.config.js" {6-9,12}
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
keyframes: {
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp"
|
||||
```
|
||||
|
||||
```tsx
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
render={({ slots }) => (
|
||||
<>
|
||||
<InputOTPGroup>
|
||||
{slots.slice(0, 3).map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
{slots.slice(3).map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Pattern
|
||||
|
||||
Use the `pattern` prop to define a custom pattern for the OTP input.
|
||||
|
||||
<ComponentPreview name="input-otp-pattern" />
|
||||
|
||||
```tsx showLineNumbers {1,7}
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"
|
||||
|
||||
...
|
||||
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup>
|
||||
{slots.map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Separator
|
||||
|
||||
You can use the `<InputOTPSeparator />` component to add a separator between the input groups.
|
||||
|
||||
<ComponentPreview name="input-otp-separator" />
|
||||
|
||||
```tsx showLineNumbers {4,17}
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/new-york/ui/input-otp"
|
||||
|
||||
...
|
||||
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup className="gap-2">
|
||||
{slots.map((slot, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<InputOTPSlot className="rounded-md border" {...slot} />
|
||||
{index !== slots.length - 1 && <InputOTPSeparator />}
|
||||
</React.Fragment>
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Controlled
|
||||
|
||||
You can use the `value` and `onChange` props to control the input value.
|
||||
|
||||
<ComponentPreview name="input-otp-controlled" />
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="input-otp-form" />
|
||||
@@ -58,6 +58,7 @@
|
||||
"embla-carousel-autoplay": "8.0.0-rc15",
|
||||
"embla-carousel-react": "8.0.0-rc15",
|
||||
"geist": "^1.1.0",
|
||||
"input-otp": "^1.0.1",
|
||||
"jotai": "^2.1.0",
|
||||
"lodash.template": "^4.5.0",
|
||||
"lucide-react": "0.288.0",
|
||||
|
||||
@@ -219,6 +219,16 @@
|
||||
],
|
||||
"type": "components:ui"
|
||||
},
|
||||
{
|
||||
"name": "input-otp",
|
||||
"dependencies": [
|
||||
"input-otp"
|
||||
],
|
||||
"files": [
|
||||
"ui/input-otp.tsx"
|
||||
],
|
||||
"type": "components:ui"
|
||||
},
|
||||
{
|
||||
"name": "label",
|
||||
"dependencies": [
|
||||
|
||||
13
apps/www/public/registry/styles/default/input-otp.json
Normal file
13
apps/www/public/registry/styles/default/input-otp.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "input-otp",
|
||||
"dependencies": [
|
||||
"input-otp"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"name": "input-otp.tsx",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"type": "components:ui"
|
||||
}
|
||||
13
apps/www/public/registry/styles/new-york/input-otp.json
Normal file
13
apps/www/public/registry/styles/new-york/input-otp.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "input-otp",
|
||||
"dependencies": [
|
||||
"input-otp"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"name": "input-otp.tsx",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"type": "components:ui"
|
||||
}
|
||||
37
apps/www/registry/default/example/input-otp-controlled.tsx
Normal file
37
apps/www/registry/default/example/input-otp-controlled.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/default/ui/input-otp"
|
||||
|
||||
export default function InputOTPControlled() {
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={value}
|
||||
onChange={(value) => setValue(value)}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup>
|
||||
{slots.map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
/>
|
||||
<div className="text-center text-sm">
|
||||
{value === "" ? (
|
||||
<>Enter your one-time password.</>
|
||||
) : (
|
||||
<>You entered: {value}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
29
apps/www/registry/default/example/input-otp-demo.tsx
Normal file
29
apps/www/registry/default/example/input-otp-demo.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/default/ui/input-otp"
|
||||
|
||||
export default function InputOTPDemo() {
|
||||
return (
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
render={({ slots }) => (
|
||||
<>
|
||||
<InputOTPGroup>
|
||||
{slots.slice(0, 3).map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
{slots.slice(3).map((slot, index) => (
|
||||
<InputOTPSlot key={index + 3} {...slot} />
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
83
apps/www/registry/default/example/input-otp-form.tsx
Normal file
83
apps/www/registry/default/example/input-otp-form.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/registry/default/ui/form"
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/default/ui/input-otp"
|
||||
import { toast } from "@/registry/default/ui/use-toast"
|
||||
|
||||
const FormSchema = z.object({
|
||||
pin: z.string().min(6, {
|
||||
message: "Your one-time password must be 6 characters.",
|
||||
}),
|
||||
})
|
||||
|
||||
export default function InputOTPForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
pin: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pin"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>One-Time Password</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup>
|
||||
{slots.map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please enter the one-time password sent to your phone.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
23
apps/www/registry/default/example/input-otp-pattern.tsx
Normal file
23
apps/www/registry/default/example/input-otp-pattern.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"
|
||||
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/default/ui/input-otp"
|
||||
|
||||
export default function InputOTPPattern() {
|
||||
return (
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup>
|
||||
{slots.map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
26
apps/www/registry/default/example/input-otp-separator.tsx
Normal file
26
apps/www/registry/default/example/input-otp-separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react"
|
||||
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/default/ui/input-otp"
|
||||
|
||||
export default function InputOTPWithSeparator() {
|
||||
return (
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup className="gap-2">
|
||||
{slots.map((slot, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<InputOTPSlot className="rounded-md border" {...slot} />
|
||||
{index !== slots.length - 1 && <InputOTPSeparator />}
|
||||
</React.Fragment>
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
64
apps/www/registry/default/ui/input-otp.tsx
Normal file
64
apps/www/registry/default/ui/input-otp.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { OTPInput, SlotProps } from "input-otp"
|
||||
import { Dot } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn("flex items-center gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
InputOTP.displayName = "InputOTP"
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = "InputOTPGroup"
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
SlotProps & React.ComponentPropsWithoutRef<"div">
|
||||
>(({ char, hasFakeCaret, isActive, className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
isActive && "z-10 ring-2 ring-offset-background ring-ring",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
InputOTPSlot.displayName = "InputOTPSlot"
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
37
apps/www/registry/new-york/example/input-otp-controlled.tsx
Normal file
37
apps/www/registry/new-york/example/input-otp-controlled.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/new-york/ui/input-otp"
|
||||
|
||||
export default function InputOTPControlled() {
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={value}
|
||||
onChange={(value) => setValue(value)}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup>
|
||||
{slots.map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
/>
|
||||
<div className="text-center text-sm">
|
||||
{value === "" ? (
|
||||
<>Enter your one-time password.</>
|
||||
) : (
|
||||
<>You entered: {value}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
29
apps/www/registry/new-york/example/input-otp-demo.tsx
Normal file
29
apps/www/registry/new-york/example/input-otp-demo.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/new-york/ui/input-otp"
|
||||
|
||||
export default function InputOTPDemo() {
|
||||
return (
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
render={({ slots }) => (
|
||||
<>
|
||||
<InputOTPGroup>
|
||||
{slots.slice(0, 3).map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
{slots.slice(3).map((slot, index) => (
|
||||
<InputOTPSlot key={index + 3} {...slot} />
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
83
apps/www/registry/new-york/example/input-otp-form.tsx
Normal file
83
apps/www/registry/new-york/example/input-otp-form.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Button } from "@/registry/new-york/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/registry/new-york/ui/form"
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/new-york/ui/input-otp"
|
||||
import { toast } from "@/registry/new-york/ui/use-toast"
|
||||
|
||||
const FormSchema = z.object({
|
||||
pin: z.string().min(6, {
|
||||
message: "Your one-time password must be 6 characters.",
|
||||
}),
|
||||
})
|
||||
|
||||
export default function InputOTPForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
pin: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pin"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>One-Time Password</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup>
|
||||
{slots.map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please enter the one-time password sent to your phone.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
23
apps/www/registry/new-york/example/input-otp-pattern.tsx
Normal file
23
apps/www/registry/new-york/example/input-otp-pattern.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"
|
||||
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/new-york/ui/input-otp"
|
||||
|
||||
export default function InputOTPPattern() {
|
||||
return (
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup>
|
||||
{slots.map((slot, index) => (
|
||||
<InputOTPSlot key={index} {...slot} />
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
26
apps/www/registry/new-york/example/input-otp-separator.tsx
Normal file
26
apps/www/registry/new-york/example/input-otp-separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react"
|
||||
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/registry/new-york/ui/input-otp"
|
||||
|
||||
export default function InputOTPWithSeparator() {
|
||||
return (
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
render={({ slots }) => (
|
||||
<InputOTPGroup className="gap-2">
|
||||
{slots.map((slot, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<InputOTPSlot className="rounded-md border" {...slot} />
|
||||
{index !== slots.length - 1 && <InputOTPSeparator />}
|
||||
</React.Fragment>
|
||||
))}{" "}
|
||||
</InputOTPGroup>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
64
apps/www/registry/new-york/ui/input-otp.tsx
Normal file
64
apps/www/registry/new-york/ui/input-otp.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { DashIcon } from "@radix-ui/react-icons"
|
||||
import { OTPInput, SlotProps } from "input-otp"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn("flex items-center gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
InputOTP.displayName = "InputOTP"
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = "InputOTPGroup"
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
SlotProps & React.ComponentPropsWithoutRef<"div">
|
||||
>(({ char, hasFakeCaret, isActive, className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
isActive && "z-10 ring-1 ring-ring",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
InputOTPSlot.displayName = "InputOTPSlot"
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<DashIcon />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
@@ -134,6 +134,12 @@ const ui: Registry = [
|
||||
type: "components:ui",
|
||||
files: ["ui/input.tsx"],
|
||||
},
|
||||
{
|
||||
name: "input-otp",
|
||||
type: "components:ui",
|
||||
dependencies: ["input-otp"],
|
||||
files: ["ui/input-otp.tsx"],
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
type: "components:ui",
|
||||
@@ -697,6 +703,36 @@ const example: Registry = [
|
||||
registryDependencies: ["input", "button", "label"],
|
||||
files: ["example/input-with-text.tsx"],
|
||||
},
|
||||
{
|
||||
name: "input-otp-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
files: ["example/input-otp-demo.tsx"],
|
||||
},
|
||||
{
|
||||
name: "input-otp-pattern",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
files: ["example/input-otp-pattern.tsx"],
|
||||
},
|
||||
{
|
||||
name: "input-otp-separator",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
files: ["example/input-otp-separator.tsx"],
|
||||
},
|
||||
{
|
||||
name: "input-otp-controlled",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp"],
|
||||
files: ["example/input-otp-controlled.tsx"],
|
||||
},
|
||||
{
|
||||
name: "input-otp-form",
|
||||
type: "components:example",
|
||||
registryDependencies: ["input-otp", "form"],
|
||||
files: ["example/input-otp-form.tsx"],
|
||||
},
|
||||
{
|
||||
name: "label-demo",
|
||||
type: "components:example",
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -230,6 +230,9 @@ importers:
|
||||
geist:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(next@14.0.4)
|
||||
input-otp:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1(react-dom@18.2.0)(react@18.2.0)
|
||||
jotai:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0(react@18.2.0)
|
||||
@@ -1472,10 +1475,6 @@ packages:
|
||||
peerDependencies:
|
||||
'@effect-ts/otel-node': '*'
|
||||
peerDependenciesMeta:
|
||||
'@effect-ts/core':
|
||||
optional: true
|
||||
'@effect-ts/otel':
|
||||
optional: true
|
||||
'@effect-ts/otel-node':
|
||||
optional: true
|
||||
dependencies:
|
||||
@@ -7616,6 +7615,16 @@ packages:
|
||||
resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
|
||||
dev: false
|
||||
|
||||
/input-otp@1.0.1(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-AFMGRsOwXcH7koO+8nnVcJFYEe92tNmRlb2TUKbj9Bpdyc44GaS3LfJam3MdoXQv1jejpMS0+fxJFSCsEDHd9A==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/internal-slot@1.0.5:
|
||||
resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
@@ -67,10 +67,15 @@ module.exports = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user