| 1 | import { OTPInput, OTPInputContext } from "input-otp"; |
| 2 | import { type ComponentProps, useContext } from "react"; |
| 3 | |
| 4 | import { cn } from "../../lib/cn"; |
| 5 | |
| 6 | // shadcn/ui's input OTP, styled with g1t's tokens: a one-time code drawn as |
| 7 | // a box per character over one real <input>, so autofill, password |
| 8 | // managers and pasting work as with any field, and the form posts the |
| 9 | // input's `name` as one value. Put slots in an InputOTPGroup, the groups |
| 10 | // apart with an InputOTPSeparator. `aria-invalid` on the InputOTP marks |
| 11 | // every box. |
| 12 | |
| 13 | export function InputOTP({ className, containerClassName, ...props }: ComponentProps<typeof OTPInput>) { |
| 14 | return ( |
| 15 | <OTPInput |
| 16 | data-slot="input-otp" |
| 17 | containerClassName={cn("group/otp flex items-center gap-2 has-disabled:opacity-50", containerClassName)} |
| 18 | className={cn("disabled:cursor-not-allowed", className)} |
| 19 | {...props} |
| 20 | /> |
| 21 | ); |
| 22 | } |
| 23 | |
| 24 | export function InputOTPGroup({ className, ...props }: ComponentProps<"div">) { |
| 25 | return <div data-slot="input-otp-group" className={cn("flex items-center", className)} {...props} />; |
| 26 | } |
| 27 | |
| 28 | export function InputOTPSlot({ index, className, ...props }: ComponentProps<"div"> & { index: number }) { |
| 29 | const context = useContext(OTPInputContext); |
| 30 | const { char, hasFakeCaret, isActive } = context?.slots[index] ?? {}; |
| 31 | return ( |
| 32 | <div |
| 33 | data-slot="input-otp-slot" |
| 34 | data-active={isActive} |
| 35 | className={cn( |
| 36 | "relative flex h-12 w-10 items-center justify-center border-y border-r border-line bg-bg font-mono text-xl text-fg tabular-nums transition-[border-color,box-shadow] sm:h-13 sm:w-11", |
| 37 | "first:rounded-l-md first:border-l last:rounded-r-md", |
| 38 | "data-[active=true]:z-10 data-[active=true]:border-accent-dim data-[active=true]:ring-2 data-[active=true]:ring-accent/25", |
| 39 | "group-has-aria-invalid/otp:border-danger/70", |
| 40 | className, |
| 41 | )} |
| 42 | {...props} |
| 43 | > |
| 44 | {char} |
| 45 | {hasFakeCaret && ( |
| 46 | <div aria-hidden="true" className="pointer-events-none absolute inset-0 flex items-center justify-center"> |
| 47 | <div className="h-5 w-px animate-caret-blink bg-fg motion-reduce:animate-none" /> |
| 48 | </div> |
| 49 | )} |
| 50 | </div> |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | export function InputOTPSeparator(props: ComponentProps<"div">) { |
| 55 | return ( |
| 56 | <div data-slot="input-otp-separator" role="separator" className="h-px w-2.5 shrink-0 bg-line-strong" {...props} /> |
| 57 | ); |
| 58 | } |