Skip to content

Commit ba69050

Browse files
committed
chore(shadcn): Add OTP component
1 parent 14a5218 commit ba69050

File tree

3 files changed

+1572
-183
lines changed

3 files changed

+1572
-183
lines changed

packages/shadcn/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@radix-ui/react-slot": "^1.2.3",
4343
"class-variance-authority": "^0.7.1",
4444
"clsx": "^2.1.1",
45+
"input-otp": "^1.4.2",
4546
"lucide-react": "^0.544.0",
4647
"react-hook-form": "^7.64.0",
4748
"tailwind-merge": "^3.3.1",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as React from "react"
2+
import { OTPInput, OTPInputContext } from "input-otp"
3+
import { MinusIcon } from "lucide-react"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
function InputOTP({
8+
className,
9+
containerClassName,
10+
...props
11+
}: React.ComponentProps<typeof OTPInput> & {
12+
containerClassName?: string
13+
}) {
14+
return (
15+
<OTPInput
16+
data-slot="input-otp"
17+
containerClassName={cn(
18+
"flex items-center gap-2 has-disabled:opacity-50",
19+
containerClassName
20+
)}
21+
className={cn("disabled:cursor-not-allowed", className)}
22+
{...props}
23+
/>
24+
)
25+
}
26+
27+
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
28+
return (
29+
<div
30+
data-slot="input-otp-group"
31+
className={cn("flex items-center", className)}
32+
{...props}
33+
/>
34+
)
35+
}
36+
37+
function InputOTPSlot({
38+
index,
39+
className,
40+
...props
41+
}: React.ComponentProps<"div"> & {
42+
index: number
43+
}) {
44+
const inputOTPContext = React.useContext(OTPInputContext)
45+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
46+
47+
return (
48+
<div
49+
data-slot="input-otp-slot"
50+
data-active={isActive}
51+
className={cn(
52+
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
53+
className
54+
)}
55+
{...props}
56+
>
57+
{char}
58+
{hasFakeCaret && (
59+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
60+
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
61+
</div>
62+
)}
63+
</div>
64+
)
65+
}
66+
67+
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
68+
return (
69+
<div data-slot="input-otp-separator" role="separator" {...props}>
70+
<MinusIcon />
71+
</div>
72+
)
73+
}
74+
75+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

0 commit comments

Comments
 (0)