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:
shadcn
2024-03-07 22:57:33 +04:00
committed by GitHub
parent 7f0af435e1
commit e8856d1dea
22 changed files with 907 additions and 4 deletions

View File

@@ -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",

View File

@@ -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",

View 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" />

View File

@@ -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",

View File

@@ -219,6 +219,16 @@
],
"type": "components:ui"
},
{
"name": "input-otp",
"dependencies": [
"input-otp"
],
"files": [
"ui/input-otp.tsx"
],
"type": "components:ui"
},
{
"name": "label",
"dependencies": [

View 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"
}

View 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"
}

View 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>
)
}

View 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>
</>
)}
/>
)
}

View 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>
)
}

View 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>
)}
/>
)
}

View 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>
)}
/>
)
}

View 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 }

View 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>
)
}

View 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>
</>
)}
/>
)
}

View 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>
)
}

View 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>
)}
/>
)
}

View 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>
)}
/>
)
}

View 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 }

View File

@@ -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
View File

@@ -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'}

View File

@@ -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",
},
},
},