Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

58 linesCodeBlameRaw
1import { OTPInput, OTPInputContext } from "input-otp";
2import { type ComponentProps, useContext } from "react";
3
4import { 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
13export 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
24export function InputOTPGroup({ className, ...props }: ComponentProps<"div">) {
25 return <div data-slot="input-otp-group" className={cn("flex items-center", className)} {...props} />;
26}
27
28export 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
54export 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}