# OTP Input Redesign Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Replace the custom OtpInput component with shadcn's InputOTP backed by `input-otp`, keeping the same external API so consumers need zero changes. **Architecture:** Install `input-otp` → generate shadcn `input-otp.tsx` primitives → rewrite `OtpInput` molecule internals using those primitives. All 3 consumers (LoginOtpStep, OtpStep, VerificationStep) continue using the same `` props interface unchanged. **Tech Stack:** `input-otp` library, shadcn/ui primitives, Tailwind v4, React 19 --- ### Task 1: Install `input-otp` dependency **Files:** - Modify: `apps/portal/package.json` **Step 1: Install the package** Run: ```bash pnpm --filter @customer-portal/portal add input-otp ``` **Step 2: Verify installation** Run: ```bash pnpm --filter @customer-portal/portal exec -- node -e "require('input-otp')" 2>&1 || echo "ESM module, checking package exists" && ls node_modules/input-otp/package.json ``` Expected: Package exists in node_modules. **Step 3: Commit** ```bash git add apps/portal/package.json pnpm-lock.yaml git commit -m "chore: add input-otp dependency" ``` --- ### Task 2: Create shadcn InputOTP primitives **Files:** - Create: `apps/portal/src/components/ui/input-otp.tsx` The shadcn CLI may not work cleanly because `components.json` aliases `utils` to `@/lib/utils` which doesn't exist. Create the component manually based on shadcn's new-york input-otp source, using the project's `@/shared/utils` import for `cn`. **Step 1: Create the primitives file** Create `apps/portal/src/components/ui/input-otp.tsx`: ```tsx "use client"; import * as React from "react"; import { OTPInput, OTPInputContext } from "input-otp"; import { MinusIcon } from "lucide-react"; import { cn } from "@/shared/utils"; function InputOTP({ className, containerClassName, ...props }: React.ComponentProps & { containerClassName?: string }) { return ( ); } function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) { return (
); } function InputOTPSlot({ index, className, ...props }: React.ComponentProps<"div"> & { index: number }) { const inputOTPContext = React.useContext(OTPInputContext); const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]; return (
{char} {hasFakeCaret && (
)}
); } function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { return (
); } export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; ``` **Step 2: Add caret-blink animation to globals.css** Add inside the `@theme` block in `apps/portal/src/app/globals.css`, before the closing `}`: ```css /* OTP caret blink animation */ --animate-caret-blink: caret-blink 1.2s ease-out infinite; @keyframes caret-blink { 0%, 70%, 100% { opacity: 1; } 20%, 50% { opacity: 0; } } ``` **Step 3: Verify type-check** Run: ```bash pnpm type-check ``` Expected: PASS **Step 4: Commit** ```bash git add apps/portal/src/components/ui/input-otp.tsx apps/portal/src/app/globals.css git commit -m "feat: add shadcn InputOTP primitives with caret-blink animation" ``` --- ### Task 3: Rewrite OtpInput molecule **Files:** - Rewrite: `apps/portal/src/components/molecules/OtpInput/OtpInput.tsx` **Step 1: Replace the entire file** Replace `apps/portal/src/components/molecules/OtpInput/OtpInput.tsx` with: ```tsx /** * OtpInput - 6-digit OTP code input * * Wrapper around shadcn InputOTP (backed by input-otp library). * Provides mobile SMS autofill, paste support, and accessible slot rendering. */ "use client"; import { REGEXP_ONLY_DIGITS } from "input-otp"; import { cn } from "@/shared/utils"; import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator, } from "@/components/ui/input-otp"; interface OtpInputProps { length?: number | undefined; value: string; onChange: (value: string) => void; onComplete?: ((value: string) => void) | undefined; disabled?: boolean | undefined; error?: string | undefined; autoFocus?: boolean | undefined; } export function OtpInput({ length = 6, value, onChange, onComplete, disabled = false, error, autoFocus = true, }: OtpInputProps) { const firstGroupSize = Math.ceil(length / 2); const secondGroupSize = length - firstGroupSize; return (
{Array.from({ length: firstGroupSize }, (_, i) => ( ))} {secondGroupSize > 0 && ( <> {Array.from({ length: secondGroupSize }, (_, i) => ( ))} )}
{error && (

{error}

)}
); } ``` Note: The `index.ts` barrel file (`export { OtpInput } from "./OtpInput"`) stays unchanged. **Step 2: Verify type-check** Run: ```bash pnpm type-check ``` Expected: PASS **Step 3: Verify lint** Run: ```bash pnpm lint ``` Expected: PASS (no unused imports, no deep path violations) **Step 4: Commit** ```bash git add apps/portal/src/components/molecules/OtpInput/OtpInput.tsx git commit -m "refactor: replace custom OtpInput with shadcn InputOTP" ``` --- ### Task 4: Verify all consumers still compile and work **Files:** (read-only verification, no changes expected) - `apps/portal/src/features/auth/components/LoginOtpStep/LoginOtpStep.tsx` - `apps/portal/src/features/services/components/eligibility-check/steps/OtpStep.tsx` - `apps/portal/src/features/get-started/components/GetStartedForm/steps/VerificationStep.tsx` **Step 1: Full type-check** Run: ```bash pnpm type-check ``` Expected: PASS — all 3 consumers use the same `OtpInputProps` API which hasn't changed. **Step 2: Run portal tests** Run: ```bash pnpm --filter @customer-portal/portal test 2>&1 | tail -20 ``` Expected: All tests pass. **Step 3: Final commit if any fixes were needed** Only commit if adjustments were required. Otherwise, skip.