diff --git a/src/components/form/form/form.stories.tsx b/src/components/form/form/form.stories.tsx
index 81f49b2..d036f94 100644
--- a/src/components/form/form/form.stories.tsx
+++ b/src/components/form/form/form.stories.tsx
@@ -2,6 +2,7 @@ import { fn } from "@storybook/test";
import { FormComponent } from "./form.component";
import { InputComponent, SelectorComponent, ButtonComponent } from "../index";
import { BoxComponent, CardComponent } from "../../../components";
+import { OtpComponent } from "../otp";
export default {
title: "Components/Form",
@@ -50,6 +51,7 @@ export const Primary = {
type="password"
placeholder="repeat password"
/>
+
&
+ React.HTMLProps;
+
+export const OtpComponent: React.FC = ({
+ className,
+ wrapperClassName,
+ placeholder,
+ bordered,
+ length = 6,
+ ...props
+}) => {
+ const id = useId();
+ const [otherProps, boxProps] = extractBoxProps(props);
+
+ const otpValueRef = useRef([]);
+ const hiddenInputRef = useRef(null);
+
+ const onChangeInput = useCallback(
+ (index: number) => (event) => {
+ if (!parseInt(event.target.value)) {
+ event.target.value = null;
+ return event.preventDefault();
+ }
+ try {
+ document.activeElement.nextElementSibling.focus();
+ } catch (e) {}
+ otpValueRef.current[index] = event.target.value;
+ hiddenInputRef.current.value = otpValueRef.current.join("");
+ },
+ [],
+ );
+
+ const inputRender = useMemo(() => {
+ const inputs = [];
+ for (let i = 0; i < length; i++) {
+ inputs.push(
+ ,
+ );
+ }
+ return inputs;
+ }, [length, onChangeInput, id, className]);
+
+ return (
+
+
+
+ {inputRender}
+
+ );
+};
diff --git a/src/components/form/otp/otp.module.scss b/src/components/form/otp/otp.module.scss
new file mode 100644
index 0000000..1a065a3
--- /dev/null
+++ b/src/components/form/otp/otp.module.scss
@@ -0,0 +1,71 @@
+@use "../../../styles/all.module" as *;
+
+.inputWrapper {
+ display: inline-flex;
+ position: relative;
+ width: 100%;
+ flex-direction: column;
+
+ .placeholder {
+ color: var(--input-placeholder-c);
+ padding: 0;
+ cursor: text;
+ user-select: none;
+ margin-left: 0rem;
+ }
+
+ .inputs {
+ display: flex;
+ gap: 0.5rem;
+ width: 100%;
+
+ .input {
+ background-color: var(--input-bg);
+ color: var(--input-c);
+ width: 3rem;
+ border: 0.2rem solid transparent;
+
+ padding: 1rem 0;
+ border-radius: var(--border-radius);
+ outline: none;
+ text-align: center;
+
+ display: inline-flex;
+
+ &.hasPlaceholder {
+ transition: padding;
+ transition-duration: 0.3s;
+
+ &:not(:placeholder-shown),
+ :-webkit-autofill {
+ padding-top: 1.7rem;
+ padding-bottom: 0.3rem;
+ }
+ }
+
+ &:active,
+ &:focus {
+ background-color: var(--input-focus-bg);
+ color: var(--input-focus-c);
+ }
+ }
+
+ &.bordered .input {
+ border: 0.2rem solid var(--input-bd);
+
+ &:active,
+ &:focus {
+ border: 0.2rem solid var(--input-focus-bd);
+ }
+ }
+ &.disabled {
+ opacity: 0.5;
+ }
+
+ // Weird hack to style autofill color in Chrome
+ .input:-webkit-autofill {
+ box-shadow: 0 0 0 1000px var(--input-bg) inset;
+ -webkit-text-fill-color: var(--input-c);
+ }
+ }
+}
diff --git a/src/components/form/otp/otp.stories.ts b/src/components/form/otp/otp.stories.ts
new file mode 100644
index 0000000..b28b895
--- /dev/null
+++ b/src/components/form/otp/otp.stories.ts
@@ -0,0 +1,19 @@
+import { fn } from "@storybook/test";
+import { OtpComponent } from "./otp.component";
+
+export default {
+ title: "Components/Form/OTP Input",
+ component: OtpComponent,
+ parameters: {
+ layout: "centered",
+ backgrounds: { default: "dark" },
+ },
+ args: { onChange: fn() },
+};
+
+export const Primary = {
+ args: {
+ placeholder: "2FA",
+ name: "otp",
+ },
+};