input-otp.tsx 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. 'use client';
  2. import * as React from 'react';
  3. import { OTPInput, OTPInputContext } from 'input-otp';
  4. import { cn } from '@/lib/utils';
  5. import { MinusIcon } from 'lucide-react';
  6. function InputOTP({
  7. className,
  8. containerClassName,
  9. ...props
  10. }: React.ComponentProps<typeof OTPInput> & {
  11. containerClassName?: string;
  12. }) {
  13. return (
  14. <OTPInput
  15. data-slot="input-otp"
  16. containerClassName={cn(
  17. 'cn-input-otp flex items-center has-disabled:opacity-50',
  18. containerClassName,
  19. )}
  20. spellCheck={false}
  21. className={cn('disabled:cursor-not-allowed', className)}
  22. {...props}
  23. />
  24. );
  25. }
  26. function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
  27. return (
  28. <div
  29. data-slot="input-otp-group"
  30. className={cn(
  31. 'has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive rounded-lg has-aria-invalid:ring-3 flex items-center',
  32. className,
  33. )}
  34. {...props}
  35. />
  36. );
  37. }
  38. function InputOTPSlot({
  39. index,
  40. className,
  41. ...props
  42. }: React.ComponentProps<'div'> & {
  43. index: number;
  44. }) {
  45. const inputOTPContext = React.useContext(OTPInputContext);
  46. const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
  47. return (
  48. <div
  49. data-slot="input-otp-slot"
  50. data-active={isActive}
  51. className={cn(
  52. 'dark:bg-input/30 border-input 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 size-8 border-y border-r text-sm transition-all outline-none first:rounded-l-lg first:border-l last:rounded-r-lg data-[active=true]:ring-3 relative flex items-center justify-center data-[active=true]:z-10',
  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. function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
  67. return (
  68. <div
  69. data-slot="input-otp-separator"
  70. className="[&_svg:not([class*='size-'])]:size-4 flex items-center"
  71. role="separator"
  72. {...props}
  73. >
  74. <MinusIcon />
  75. </div>
  76. );
  77. }
  78. export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };