commit 05dc2565972ada733a8d57102d551739d0f10a18
parent 639e92f19de6e3efa3fa782637632d2e0f45d865
Author: Daniel D’Aquino <daniel@daquino.me>
Date: Mon, 1 Apr 2024 14:21:27 +0000
Setup OTP input element
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
4 files changed, 105 insertions(+), 0 deletions(-)
diff --git a/package-lock.json b/package-lock.json
@@ -16,6 +16,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"framer-motion": "^10.16.4",
+ "input-otp": "^1.2.3",
"lnmessage": "^0.2.7",
"lucide-react": "^0.287.0",
"next": "13.5.4",
@@ -3163,6 +3164,15 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "node_modules/input-otp": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.2.3.tgz",
+ "integrity": "sha512-pxYvgnihL9KAdpcShX2+iKctdMRbDs36bIqd8uIsN3e5vv9VjMv2bhO3S5Bl1PjcDPsA/OXZe5R71n8oVtucfQ==",
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ }
+ },
"node_modules/internal-slot": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
diff --git a/package.json b/package.json
@@ -22,6 +22,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"framer-motion": "^10.16.4",
+ "input-otp": "^1.2.3",
"lnmessage": "^0.2.7",
"lucide-react": "^0.287.0",
"next": "13.5.4",
diff --git a/src/components/ui/InputOTP.tsx b/src/components/ui/InputOTP.tsx
@@ -0,0 +1,89 @@
+"use client"
+
+import * as React from "react"
+import { OTPInput, OTPInputContext } 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, containerClassName, ...props }, ref) => (
+ <OTPInput
+ ref={ref}
+ containerClassName={cn(
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
+ containerClassName
+ )}
+ className={cn("disabled:cursor-not-allowed", 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">,
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
+>(({ index, className, ...props }, ref) => {
+ const inputOTPContext = React.useContext(OTPInputContext)
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
+
+ return (
+ <div
+ ref={ref}
+ className={cn(
+ "relative flex h-10 w-10 items-center justify-center border-y border-r border-purple-200/50 text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md text-white",
+ isActive && "z-10 ring-2 ring-ring ring-offset-background",
+ className
+ )}
+ {...props}
+ >
+ {char}
+ {hasFakeCaret && (
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
+ <div className="h-4 w-px animate-caret-blink bg-white 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 className="text-purple-200/50" />
+ </div>
+))
+InputOTPSeparator.displayName = "InputOTPSeparator"
+
+function InputOTP6Digits({ ...props }) {
+ return (
+ <InputOTP maxLength={6} {...props}>
+ <InputOTPGroup>
+ <InputOTPSlot index={0} />
+ <InputOTPSlot index={1} />
+ <InputOTPSlot index={2} />
+ </InputOTPGroup>
+ <InputOTPSeparator />
+ <InputOTPGroup>
+ <InputOTPSlot index={3} />
+ <InputOTPSlot index={4} />
+ <InputOTPSlot index={5} />
+ </InputOTPGroup>
+ </InputOTP>
+ )
+}
+
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator, InputOTP6Digits }
diff --git a/tailwind.config.ts b/tailwind.config.ts
@@ -23,10 +23,15 @@ const config: Config = {
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",
},
},
colors: {