Productivity is the result of a commitment to excellence, intelligent planning, and focused effort.

The full working code is below, including the CSS

import { useEffect, useRef, useState } from "react";
import styles from "./typewritter-effect.module.scss";

const DELAY = 180; // milliseconds
const DEFAULT_TEXT = "Hello there! This is a typewriter effect animation.";

const Typewritter = () => {
  const output = useRef(null);
  const [timeoutExp, setTimeoutExp] = useState<null | number>(null);
  const [text, setText] = useState<string>(DEFAULT_TEXT);

  const hardReset = () => {
    clearTimeout(timeoutExp);
    setTimeoutExp(null);
    setText("");
    if (output.current) {
      output.current.innerHTML = "";
    }
  };

  const animate = () => {
    if (!output?.current || !text) {
      return null;
    }

    let index = 0;

    function typeNextLetter() {
      if (index < text.length) {
        if (output?.current) {
          output.current.innerHTML += text.charAt(index);
        }

        index++;
        const timeout = setTimeout(typeNextLetter, DELAY, "myid");
        setTimeoutExp(timeout);
      }
    }

    typeNextLetter();
  };

  useEffect(() => {
    animate();

    return () => clearTimeout(timeoutExp);
  }, []);

  const onChange = (event) => {
    setText(event?.currentTarget?.value ?? "");
  };

  const onSubmit = () => {
    setTimeout(() => {
      animate();
    }, DELAY);

    hardReset();
  };

  return (
    <div className={styles.wrapper}>
      <div ref={output} id="output" className={styles.neonText} />
      <textarea
        onChange={onChange}
        className={styles.textarea}
        value={text}
        placeholder="Try with your own text"
        rows={3}
      />
      <div className={styles.actions}>
        <button className={styles.button} onClick={hardReset}>
          Clear
        </button>
        <button className={styles.button} onClick={onSubmit}>
          Submit
        </button>
      </div>
    </div>
  );
};

export default Typewritter;
The CSS file in my case is named typewritter-effect.module.scss

@import url("https://fonts.googleapis.com/css?family=Sacramento&display=swap");

.wrapper {
  display: flex;
  flex-flow: column nowrap;
  align-items: center;
  justify-content: center;
  background: black;
  padding-top: 60px;
  padding-bottom: 60px;
  min-height: 70vh; /* fallback */
  min-height: 70dvh; /* dynamic viewport height */
}

.neonText {
  font-size: 4rem;
  text-shadow: 0 0 5px #ffa500, 0 0 15px #ffa500, 0 0 20px #ffa500,
    0 0 40px #ffa500, 0 0 60px #ff0000, 0 0 10px #ff8d00, 0 0 98px #ff0000;
  color: #fff6a9;
  font-family: "Sacramento", cursive;
  text-align: center;
  animation: blink 6s infinite;
  max-width: 600px;

  @media only screen and (max-width: 600px) {
    font-size: 2.5rem;
    max-width: 350px;
  }
}

@keyframes blink {
  20%,
  24%,
  55% {
    text-shadow: none;
  }

  0%,
  19%,
  21%,
  23%,
  25%,
  54%,
  56%,
  100% {
    text-shadow: 0 0 5px #ffa500, 0 0 15px #ffa500, 0 0 20px #ffa500,
      0 0 40px #ffa500, 0 0 60px #ff0000, 0 0 10px #ff8d00, 0 0 98px #ff0000;
  }
}

.textarea {
  margin-top: 32px;
  padding: 0.4rem 1rem;
  font-size: 1rem;
  line-height: 1;
}

.actions {
  display: flex;
  flex-flow: row nowrap;
  justify-content: space-between;
}

.button {
  color: white;
  margin: 16px;
  padding: 4px 8px;
  font-size: 1rem;
  width: 40%;
  border: 1px solid white;
}