From e8856d1dea9fc8a75e4a82ed0a67d0db6ef90c3e Mon Sep 17 00:00:00 2001 From: shadcn Date: Thu, 7 Mar 2024 22:57:33 +0400 Subject: [PATCH] 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 --- apps/www/__registry__/index.tsx | 84 ++++++++ apps/www/config/docs.ts | 6 + .../www/content/docs/components/input-otp.mdx | 202 ++++++++++++++++++ apps/www/package.json | 1 + apps/www/public/registry/index.json | 10 + .../registry/styles/default/input-otp.json | 13 ++ .../registry/styles/new-york/input-otp.json | 13 ++ .../default/example/input-otp-controlled.tsx | 37 ++++ .../default/example/input-otp-demo.tsx | 29 +++ .../default/example/input-otp-form.tsx | 83 +++++++ .../default/example/input-otp-pattern.tsx | 23 ++ .../default/example/input-otp-separator.tsx | 26 +++ apps/www/registry/default/ui/input-otp.tsx | 64 ++++++ .../new-york/example/input-otp-controlled.tsx | 37 ++++ .../new-york/example/input-otp-demo.tsx | 29 +++ .../new-york/example/input-otp-form.tsx | 83 +++++++ .../new-york/example/input-otp-pattern.tsx | 23 ++ .../new-york/example/input-otp-separator.tsx | 26 +++ apps/www/registry/new-york/ui/input-otp.tsx | 64 ++++++ apps/www/registry/registry.ts | 36 ++++ pnpm-lock.yaml | 17 +- tailwind.config.cjs | 5 + 22 files changed, 907 insertions(+), 4 deletions(-) create mode 100644 apps/www/content/docs/components/input-otp.mdx create mode 100644 apps/www/public/registry/styles/default/input-otp.json create mode 100644 apps/www/public/registry/styles/new-york/input-otp.json create mode 100644 apps/www/registry/default/example/input-otp-controlled.tsx create mode 100644 apps/www/registry/default/example/input-otp-demo.tsx create mode 100644 apps/www/registry/default/example/input-otp-form.tsx create mode 100644 apps/www/registry/default/example/input-otp-pattern.tsx create mode 100644 apps/www/registry/default/example/input-otp-separator.tsx create mode 100644 apps/www/registry/default/ui/input-otp.tsx create mode 100644 apps/www/registry/new-york/example/input-otp-controlled.tsx create mode 100644 apps/www/registry/new-york/example/input-otp-demo.tsx create mode 100644 apps/www/registry/new-york/example/input-otp-form.tsx create mode 100644 apps/www/registry/new-york/example/input-otp-pattern.tsx create mode 100644 apps/www/registry/new-york/example/input-otp-separator.tsx create mode 100644 apps/www/registry/new-york/ui/input-otp.tsx diff --git a/apps/www/__registry__/index.tsx b/apps/www/__registry__/index.tsx index 0d1ea0d7f0..a5455fa47f 100644 --- a/apps/www/__registry__/index.tsx +++ b/apps/www/__registry__/index.tsx @@ -152,6 +152,13 @@ export const Index: Record = { 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 = { 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 = { 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 = { 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", diff --git a/apps/www/config/docs.ts b/apps/www/config/docs.ts index 02a2c48b7b..4d7fd08665 100644 --- a/apps/www/config/docs.ts +++ b/apps/www/config/docs.ts @@ -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", diff --git a/apps/www/content/docs/components/input-otp.mdx b/apps/www/content/docs/components/input-otp.mdx new file mode 100644 index 0000000000..0e31d1b9b1 --- /dev/null +++ b/apps/www/content/docs/components/input-otp.mdx @@ -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 +--- + + + +## 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 + + + + + CLI + Manual + + + + + +Run the following command: + +```bash +npx shadcn-ui@latest add input-otp +``` + +Update `tailwind.config.js` + +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", + }, + }, + }, +} +``` + + + + + + + + + +Install the following dependencies: + +```bash +npm install input-otp +``` + +Copy and paste the following code into your project. + + + +Update the import paths to match your project setup. + +Update `tailwind.config.js` + +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", + }, + }, + }, +} +``` + + + + + + + +## Usage + +```tsx +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/components/ui/input-otp" +``` + +```tsx + ( + <> + + {slots.slice(0, 3).map((slot, index) => ( + + ))}{" "} + + + + {slots.slice(3).map((slot, index) => ( + + ))} + + + )} +/> +``` + +## Examples + +### Pattern + +Use the `pattern` prop to define a custom pattern for the OTP input. + + + +```tsx showLineNumbers {1,7} +import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp" + +... + + ( + + {slots.map((slot, index) => ( + + ))}{" "} + + )} +/> +``` + +### Separator + +You can use the `` component to add a separator between the input groups. + + + +```tsx showLineNumbers {4,17} +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/registry/new-york/ui/input-otp" + +... + + ( + + {slots.map((slot, index) => ( + + + {index !== slots.length - 1 && } + + ))}{" "} + + )} +/> +``` + +### Controlled + +You can use the `value` and `onChange` props to control the input value. + + + +### Form + + diff --git a/apps/www/package.json b/apps/www/package.json index 18a30ef328..38c212ce85 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -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", diff --git a/apps/www/public/registry/index.json b/apps/www/public/registry/index.json index 8464d10c14..561e0d9e6f 100644 --- a/apps/www/public/registry/index.json +++ b/apps/www/public/registry/index.json @@ -219,6 +219,16 @@ ], "type": "components:ui" }, + { + "name": "input-otp", + "dependencies": [ + "input-otp" + ], + "files": [ + "ui/input-otp.tsx" + ], + "type": "components:ui" + }, { "name": "label", "dependencies": [ diff --git a/apps/www/public/registry/styles/default/input-otp.json b/apps/www/public/registry/styles/default/input-otp.json new file mode 100644 index 0000000000..6be1abeabc --- /dev/null +++ b/apps/www/public/registry/styles/default/input-otp.json @@ -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,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n
\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 \n {char}\n {hasFakeCaret && (\n
\n
\n
\n )}\n
\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n
\n \n
\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n" + } + ], + "type": "components:ui" +} \ No newline at end of file diff --git a/apps/www/public/registry/styles/new-york/input-otp.json b/apps/www/public/registry/styles/new-york/input-otp.json new file mode 100644 index 0000000000..8bdb700821 --- /dev/null +++ b/apps/www/public/registry/styles/new-york/input-otp.json @@ -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,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n
\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 \n {char}\n {hasFakeCaret && (\n
\n
\n
\n )}\n
\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n
\n \n
\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n" + } + ], + "type": "components:ui" +} \ No newline at end of file diff --git a/apps/www/registry/default/example/input-otp-controlled.tsx b/apps/www/registry/default/example/input-otp-controlled.tsx new file mode 100644 index 0000000000..264b212cc6 --- /dev/null +++ b/apps/www/registry/default/example/input-otp-controlled.tsx @@ -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 ( +
+ setValue(value)} + render={({ slots }) => ( + + {slots.map((slot, index) => ( + + ))}{" "} + + )} + /> +
+ {value === "" ? ( + <>Enter your one-time password. + ) : ( + <>You entered: {value} + )} +
+
+ ) +} diff --git a/apps/www/registry/default/example/input-otp-demo.tsx b/apps/www/registry/default/example/input-otp-demo.tsx new file mode 100644 index 0000000000..f2b8d7fb54 --- /dev/null +++ b/apps/www/registry/default/example/input-otp-demo.tsx @@ -0,0 +1,29 @@ +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/registry/default/ui/input-otp" + +export default function InputOTPDemo() { + return ( + ( + <> + + {slots.slice(0, 3).map((slot, index) => ( + + ))}{" "} + + + + {slots.slice(3).map((slot, index) => ( + + ))} + + + )} + /> + ) +} diff --git a/apps/www/registry/default/example/input-otp-form.tsx b/apps/www/registry/default/example/input-otp-form.tsx new file mode 100644 index 0000000000..66b1870214 --- /dev/null +++ b/apps/www/registry/default/example/input-otp-form.tsx @@ -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>({ + resolver: zodResolver(FormSchema), + defaultValues: { + pin: "", + }, + }) + + function onSubmit(data: z.infer) { + toast({ + title: "You submitted the following values:", + description: ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ), + }) + } + + return ( +
+ + ( + + One-Time Password + + ( + + {slots.map((slot, index) => ( + + ))}{" "} + + )} + {...field} + /> + + + Please enter the one-time password sent to your phone. + + + + )} + /> + + + + + ) +} diff --git a/apps/www/registry/default/example/input-otp-pattern.tsx b/apps/www/registry/default/example/input-otp-pattern.tsx new file mode 100644 index 0000000000..a9454b0a0a --- /dev/null +++ b/apps/www/registry/default/example/input-otp-pattern.tsx @@ -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 ( + ( + + {slots.map((slot, index) => ( + + ))}{" "} + + )} + /> + ) +} diff --git a/apps/www/registry/default/example/input-otp-separator.tsx b/apps/www/registry/default/example/input-otp-separator.tsx new file mode 100644 index 0000000000..fd8f12c115 --- /dev/null +++ b/apps/www/registry/default/example/input-otp-separator.tsx @@ -0,0 +1,26 @@ +import React from "react" + +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/registry/default/ui/input-otp" + +export default function InputOTPWithSeparator() { + return ( + ( + + {slots.map((slot, index) => ( + + + {index !== slots.length - 1 && } + + ))}{" "} + + )} + /> + ) +} diff --git a/apps/www/registry/default/ui/input-otp.tsx b/apps/www/registry/default/ui/input-otp.tsx new file mode 100644 index 0000000000..a42ca9a1c9 --- /dev/null +++ b/apps/www/registry/default/ui/input-otp.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( +
+)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + SlotProps & React.ComponentPropsWithoutRef<"div"> +>(({ char, hasFakeCaret, isActive, className, ...props }, ref) => { + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( +
+ +
+)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/apps/www/registry/new-york/example/input-otp-controlled.tsx b/apps/www/registry/new-york/example/input-otp-controlled.tsx new file mode 100644 index 0000000000..0293a90a09 --- /dev/null +++ b/apps/www/registry/new-york/example/input-otp-controlled.tsx @@ -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 ( +
+ setValue(value)} + render={({ slots }) => ( + + {slots.map((slot, index) => ( + + ))}{" "} + + )} + /> +
+ {value === "" ? ( + <>Enter your one-time password. + ) : ( + <>You entered: {value} + )} +
+
+ ) +} diff --git a/apps/www/registry/new-york/example/input-otp-demo.tsx b/apps/www/registry/new-york/example/input-otp-demo.tsx new file mode 100644 index 0000000000..bfa24c031a --- /dev/null +++ b/apps/www/registry/new-york/example/input-otp-demo.tsx @@ -0,0 +1,29 @@ +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/registry/new-york/ui/input-otp" + +export default function InputOTPDemo() { + return ( + ( + <> + + {slots.slice(0, 3).map((slot, index) => ( + + ))}{" "} + + + + {slots.slice(3).map((slot, index) => ( + + ))} + + + )} + /> + ) +} diff --git a/apps/www/registry/new-york/example/input-otp-form.tsx b/apps/www/registry/new-york/example/input-otp-form.tsx new file mode 100644 index 0000000000..432216a7a5 --- /dev/null +++ b/apps/www/registry/new-york/example/input-otp-form.tsx @@ -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>({ + resolver: zodResolver(FormSchema), + defaultValues: { + pin: "", + }, + }) + + function onSubmit(data: z.infer) { + toast({ + title: "You submitted the following values:", + description: ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ), + }) + } + + return ( +
+ + ( + + One-Time Password + + ( + + {slots.map((slot, index) => ( + + ))}{" "} + + )} + {...field} + /> + + + Please enter the one-time password sent to your phone. + + + + )} + /> + + + + + ) +} diff --git a/apps/www/registry/new-york/example/input-otp-pattern.tsx b/apps/www/registry/new-york/example/input-otp-pattern.tsx new file mode 100644 index 0000000000..48eb4f5630 --- /dev/null +++ b/apps/www/registry/new-york/example/input-otp-pattern.tsx @@ -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 ( + ( + + {slots.map((slot, index) => ( + + ))}{" "} + + )} + /> + ) +} diff --git a/apps/www/registry/new-york/example/input-otp-separator.tsx b/apps/www/registry/new-york/example/input-otp-separator.tsx new file mode 100644 index 0000000000..c789f15541 --- /dev/null +++ b/apps/www/registry/new-york/example/input-otp-separator.tsx @@ -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 ( + ( + + {slots.map((slot, index) => ( + + + {index !== slots.length - 1 && } + + ))}{" "} + + )} + /> + ) +} diff --git a/apps/www/registry/new-york/ui/input-otp.tsx b/apps/www/registry/new-york/ui/input-otp.tsx new file mode 100644 index 0000000000..6229e3cb79 --- /dev/null +++ b/apps/www/registry/new-york/ui/input-otp.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( +
+)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + SlotProps & React.ComponentPropsWithoutRef<"div"> +>(({ char, hasFakeCaret, isActive, className, ...props }, ref) => { + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( +
+ +
+)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/apps/www/registry/registry.ts b/apps/www/registry/registry.ts index 8906ebd46a..27523d1399 100644 --- a/apps/www/registry/registry.ts +++ b/apps/www/registry/registry.ts @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd8e99e6fe..5f112a6736 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/tailwind.config.cjs b/tailwind.config.cjs index b4cdfbcaef..fc55679139 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -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", }, }, },