diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx
index 3e6cc3855b2..a93eabd926d 100644
--- a/core/src/components/input-otp/input-otp.tsx
+++ b/core/src/components/input-otp/input-otp.tsx
@@ -48,6 +48,7 @@ export class InputOTP implements ComponentInterface {
 
   @State() private inputValues: string[] = [];
   @State() hasFocus = false;
+  @State() private previousInputValues: string[] = [];
 
   /**
    * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
@@ -336,6 +337,7 @@ export class InputOTP implements ComponentInterface {
     });
     // Update the value without emitting events
     this.value = this.inputValues.join('');
+    this.previousInputValues = [...this.inputValues];
   }
 
   /**
@@ -525,19 +527,12 @@ export class InputOTP implements ComponentInterface {
   }
 
   /**
-   * Handles keyboard navigation and input for the OTP component.
+   * Handles keyboard navigation for the OTP component.
    *
    * Navigation:
    * - Backspace: Clears current input and moves to previous box if empty
    * - Arrow Left/Right: Moves focus between input boxes
    * - Tab: Allows normal tab navigation between components
-   *
-   * Input Behavior:
-   * - Validates input against the allowed pattern
-   * - When entering a key in a filled box:
-   *   - Shifts existing values right if there is room
-   *   - Updates the value of the input group
-   *   - Prevents default behavior to avoid automatic focus shift
    */
   private onKeyDown = (index: number) => (event: KeyboardEvent) => {
     const { length } = this;
@@ -595,34 +590,32 @@ export class InputOTP implements ComponentInterface {
       // Let all tab events proceed normally
       return;
     }
-
-    // If the input box contains a value and the key being
-    // entered is a valid key for the input box update the value
-    // and shift the values to the right if there is room.
-    if (this.inputValues[index] && this.validKeyPattern.test(event.key)) {
-      if (!this.inputValues[length - 1]) {
-        for (let i = length - 1; i > index; i--) {
-          this.inputValues[i] = this.inputValues[i - 1];
-          this.inputRefs[i].value = this.inputValues[i] || '';
-        }
-      }
-      this.inputValues[index] = event.key;
-      this.inputRefs[index].value = event.key;
-      this.updateValue(event);
-
-      // Prevent default to avoid the browser from
-      // automatically moving the focus to the next input
-      event.preventDefault();
-    }
   };
 
+  /**
+   * Processes all input scenarios for each input box.
+   *
+   * This function manages:
+   * 1. Autofill handling
+   * 2. Input validation
+   * 3. Full selection replacement or typing in an empty box
+   * 4. Inserting in the middle with available space (shifting)
+   * 5. Single character replacement
+   */
   private onInput = (index: number) => (event: InputEvent) => {
     const { length, validKeyPattern } = this;
-    const value = (event.target as HTMLInputElement).value;
-
-    // If the value is longer than 1 character (autofill), split it into
-    // characters and filter out invalid ones
-    if (value.length > 1) {
+    const input = event.target as HTMLInputElement;
+    const value = input.value;
+    const previousValue = this.previousInputValues[index] || '';
+
+    // 1. Autofill handling
+    // If the length of the value increases by more than 1 from the previous
+    // value, treat this as autofill. This is to prevent the case where the
+    // user is typing a single character into an input box containing a value
+    // as that will trigger this function with a value length of 2 characters.
+    const isAutofill = value.length - previousValue.length > 1;
+    if (isAutofill) {
+      // Distribute valid characters across input boxes
       const validChars = value
         .split('')
         .filter((char) => validKeyPattern.test(char))
@@ -639,8 +632,10 @@ export class InputOTP implements ComponentInterface {
         });
       }
 
-      // Update the value of the input group and emit the input change event
-      this.value = validChars.join('');
+      for (let i = 0; i < length; i++) {
+        this.inputValues[i] = validChars[i] || '';
+        this.inputRefs[i].value = validChars[i] || '';
+      }
       this.updateValue(event);
 
       // Focus the first empty input box or the last input box if all boxes
@@ -651,23 +646,85 @@ export class InputOTP implements ComponentInterface {
         this.inputRefs[nextIndex]?.focus();
       }, 20);
 
+      this.previousInputValues = [...this.inputValues];
       return;
     }
 
-    // Only allow input if it matches the pattern
-    if (value.length > 0 && !validKeyPattern.test(value)) {
-      this.inputRefs[index].value = '';
-      this.inputValues[index] = '';
+    // 2. Input validation
+    // If the character entered is invalid (does not match the pattern),
+    // restore the previous value and exit
+    if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
+      input.value = this.inputValues[index] || '';
+      this.previousInputValues = [...this.inputValues];
       return;
     }
 
-    // For single character input, fill the current box
-    this.inputValues[index] = value;
-    this.updateValue(event);
-
-    if (value.length > 0) {
+    // 3. Full selection replacement or typing in an empty box
+    // If the user selects all text in the input box and types, or if the
+    // input box is empty, replace only this input box. If the box is empty,
+    // move to the next box, otherwise stay focused on this box.
+    const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
+    const isEmpty = !this.inputValues[index];
+    if (isAllSelected || isEmpty) {
+      this.inputValues[index] = value;
+      input.value = value;
+      this.updateValue(event);
       this.focusNext(index);
+      this.previousInputValues = [...this.inputValues];
+      return;
     }
+
+    // 4. Inserting in the middle with available space (shifting)
+    // If typing in a filled input box and there are empty boxes at the end,
+    // shift all values starting at the current box to the right, and insert
+    // the new character at the current box.
+    const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
+    if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
+      // Get the inserted character (from event or by diffing value/previousValue)
+      let newChar = (event as InputEvent).data;
+      if (!newChar) {
+        newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
+      }
+      // Validate the new character before shifting
+      if (!validKeyPattern.test(newChar)) {
+        input.value = this.inputValues[index] || '';
+        this.previousInputValues = [...this.inputValues];
+        return;
+      }
+      // Shift values right from the end to the insertion point
+      for (let i = this.inputValues.length - 1; i > index; i--) {
+        this.inputValues[i] = this.inputValues[i - 1];
+        this.inputRefs[i].value = this.inputValues[i] || '';
+      }
+      this.inputValues[index] = newChar;
+      this.inputRefs[index].value = newChar;
+      this.updateValue(event);
+      this.previousInputValues = [...this.inputValues];
+      return;
+    }
+
+    // 5. Single character replacement
+    // Handles replacing a single character in a box containing a value based
+    // on the cursor position. We need the cursor position to determine which
+    // character was the last character typed. For example, if the user types "2"
+    // in an input box with the cursor at the beginning of the value of "6",
+    // the value will be "26", but we want to grab the "2" as the last character
+    // typed.
+    const cursorPos = input.selectionStart ?? value.length;
+    const newCharIndex = cursorPos - 1;
+    const newChar = value[newCharIndex] ?? value[0];
+
+    // Check if the new character is valid before updating the value
+    if (!validKeyPattern.test(newChar)) {
+      input.value = this.inputValues[index] || '';
+      this.previousInputValues = [...this.inputValues];
+      return;
+    }
+
+    this.inputValues[index] = newChar;
+    input.value = newChar;
+    this.updateValue(event);
+    this.previousInputValues = [...this.inputValues];
   };
 
   /**
@@ -711,12 +768,8 @@ export class InputOTP implements ComponentInterface {
 
     // Focus the next empty input after pasting
     // If all boxes are filled, focus the last input
-    const nextEmptyIndex = validChars.length;
-    if (nextEmptyIndex < length) {
-      inputRefs[nextEmptyIndex]?.focus();
-    } else {
-      inputRefs[length - 1]?.focus();
-    }
+    const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
+    inputRefs[nextEmptyIndex]?.focus();
   };
 
   /**
diff --git a/core/src/components/input-otp/test/basic/input-otp.e2e.ts b/core/src/components/input-otp/test/basic/input-otp.e2e.ts
index 2a50c1abd5c..2067a000209 100644
--- a/core/src/components/input-otp/test/basic/input-otp.e2e.ts
+++ b/core/src/components/input-otp/test/basic/input-otp.e2e.ts
@@ -442,6 +442,67 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
 
       await verifyInputValues(inputOtp, ['1', '9', '3', '']);
     });
+
+    test('should replace the last value when typing one more than the length', async ({ page }) => {
+      await page.setContent(`Description`, config);
+
+      const inputOtp = page.locator('ion-input-otp');
+      const firstInput = inputOtp.locator('input').first();
+      await firstInput.focus();
+
+      await page.keyboard.type('12345');
+
+      await verifyInputValues(inputOtp, ['1', '2', '3', '5']);
+    });
+
+    test('should replace the last value when typing one more than the length and the type is text', async ({
+      page,
+    }, testInfo) => {
+      testInfo.annotations.push({
+        type: 'issue',
+        description: 'https://github.com/ionic-team/ionic-framework/issues/30459',
+      });
+
+      await page.setContent(`Description`, config);
+
+      const inputOtp = page.locator('ion-input-otp');
+      const firstInput = inputOtp.locator('input').first();
+      await firstInput.focus();
+
+      await page.keyboard.type('abcde');
+
+      await verifyInputValues(inputOtp, ['a', 'b', 'c', 'e']);
+    });
+
+    test('should not insert or shift when typing an invalid character before a number', async ({ page }) => {
+      await page.setContent(`Description`, config);
+
+      const inputOtp = page.locator('ion-input-otp');
+      const firstInput = inputOtp.locator('input').first();
+      await firstInput.focus();
+
+      // Move cursor to the start of the first input
+      await firstInput.evaluate((el: HTMLInputElement) => el.setSelectionRange(0, 0));
+
+      await page.keyboard.type('w');
+
+      await verifyInputValues(inputOtp, ['1', '2', '', '']);
+    });
+
+    test('should not insert or shift when typing an invalid character after a number', async ({ page }) => {
+      await page.setContent(`Description`, config);
+
+      const inputOtp = page.locator('ion-input-otp');
+      const firstInput = inputOtp.locator('input').first();
+      await firstInput.focus();
+
+      // Move cursor to the end of the first input
+      await firstInput.evaluate((el: HTMLInputElement) => el.setSelectionRange(1, 1));
+
+      await page.keyboard.type('w');
+
+      await verifyInputValues(inputOtp, ['1', '2', '', '']);
+    });
   });
 
   test.describe(title('input-otp: autofill functionality'), () => {
@@ -460,6 +521,53 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
       await expect(lastInput).toBeFocused();
     });
 
+    test('should handle autofill correctly when all characters are the same', async ({ page }) => {
+      await page.setContent(`Description`, config);
+
+      const firstInput = page.locator('ion-input-otp input').first();
+      await firstInput.focus();
+
+      await simulateAutofill(firstInput, '1111');
+
+      const inputOtp = page.locator('ion-input-otp');
+      await verifyInputValues(inputOtp, ['1', '1', '1', '1']);
+
+      const lastInput = page.locator('ion-input-otp input').last();
+      await expect(lastInput).toBeFocused();
+    });
+
+    test('should handle autofill correctly when length is 2', async ({ page }) => {
+      await page.setContent(`Description`, config);
+
+      const firstInput = page.locator('ion-input-otp input').first();
+      await firstInput.focus();
+
+      await simulateAutofill(firstInput, '12');
+
+      const inputOtp = page.locator('ion-input-otp');
+      await verifyInputValues(inputOtp, ['1', '2']);
+
+      const lastInput = page.locator('ion-input-otp input').last();
+      await expect(lastInput).toBeFocused();
+    });
+
+    test('should handle autofill correctly when length is 2 after typing 1 character', async ({ page }) => {
+      await page.setContent(`Description`, config);
+
+      await page.keyboard.type('1');
+
+      const secondInput = page.locator('ion-input-otp input').nth(1);
+      await secondInput.focus();
+
+      await simulateAutofill(secondInput, '22');
+
+      const inputOtp = page.locator('ion-input-otp');
+      await verifyInputValues(inputOtp, ['2', '2']);
+
+      const lastInput = page.locator('ion-input-otp input').last();
+      await expect(lastInput).toBeFocused();
+    });
+
     test('should handle autofill correctly when it exceeds the length', async ({ page }) => {
       await page.setContent(`Description`, config);