Docs

Content Scan

A scanning component to highlight detected words to predict AI content probability

Installation

Install dependencies

npm install framer-motion

Update tailwind.config.js

Add the following to your tailwind.config.js file.

module.exports = {
  theme: {
    extend: {
      backgroundImage: {
        "custom-gradient": "linear-gradient(to left, rgba(136,127,242,0.7) 0%, transparent 100%)",
      },
    },
  }
}

Run the following command

It will create a new file content-scan.tsx inside the components/animata/feature-cards directory.

mkdir -p components/animata/feature-cards && touch components/animata/feature-cards/content-scan.tsx

Paste the code

Open the newly created file and paste the following code:

import React, { useEffect, useRef, useState } from "react";
import { motion, useAnimation } from "framer-motion";
 
interface ContentScannerProps {
  content: string;
  highlightWords: string[];
  scanDuration?: number;
  reverseDuration?: number;
}
 
const ContentScanner: React.FC<ContentScannerProps> = ({
  content,
  highlightWords,
  scanDuration = 3,
  reverseDuration = 1,
}) => {
  const [scanning, setScanning] = useState(false);
  const [aiProbability, setAiProbability] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);
  const scannerRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);
  const scannerAnimation = useAnimation();
  const [highlightedWords, setHighlightedWords] = useState<string[]>([]);
  const [animationPhase, setAnimationPhase] = useState<"idle" | "forward" | "paused" | "reverse">(
    "idle",
  );
 
  const startScanning = async () => {
    if (scanning || !containerRef.current) return;
 
    setScanning(true);
    setAiProbability(0);
    setHighlightedWords([]);
    setAnimationPhase("forward");
 
    const containerWidth = containerRef.current.offsetWidth - 110;
 
    // Forward scan
    await scannerAnimation.start({
      x: containerWidth,
      transition: { duration: scanDuration, ease: "linear" },
    });
 
    setAnimationPhase("paused");
 
    // Pause
    await new Promise((resolve) => setTimeout(resolve, 200));
 
    setAnimationPhase("reverse");
 
    // Backward scan
    await scannerAnimation.start({
      x: "-87%",
      transition: { duration: reverseDuration, ease: "linear" },
    });
 
    setScanning(false);
    setHighlightedWords([]);
    setAnimationPhase("idle");
  };
 
  useEffect(() => {
    let interval: NodeJS.Timeout;
    let pauseTimeout: NodeJS.Timeout;
 
    if (animationPhase === "forward") {
      interval = setInterval(
        () => {
          setAiProbability((prev) =>
            Math.min(prev + 1, Math.floor(content.length / highlightWords.length)),
          );
        },
        (scanDuration * 1000) / 55,
      );
    } else if (animationPhase === "paused") {
      //delay before starting reverse
      pauseTimeout = setTimeout(() => {
        setAnimationPhase("reverse");
      }, 200);
    } else if (animationPhase === "reverse") {
      interval = setInterval(
        () => {
          setAiProbability((prev) => Math.max(prev - 1, 0));
        },
        (reverseDuration * 1000) / 40,
      );
    }
 
    return () => {
      clearInterval(interval);
      clearTimeout(pauseTimeout);
    };
  }, [animationPhase, scanDuration, reverseDuration, content.length, highlightWords.length]);
 
  useEffect(() => {
    if (scanning && scannerRef.current && contentRef.current) {
      const updateHighlightedWords = () => {
        const scannerRect = scannerRef.current!.getBoundingClientRect();
        const contentRect = contentRef.current!.getBoundingClientRect();
        const scannerRightEdge = scannerRect.right - contentRect.left;
 
        const newHighlightedWords = highlightWords.filter((phrase) => {
          const phraseElements = contentRef.current!.querySelectorAll(`[data-phrase="${phrase}"]`);
          return Array.from(phraseElements).some((element) => {
            const elementRect = element.getBoundingClientRect();
            const elementRightEdge = elementRect.right - contentRect.left;
            return elementRightEdge <= scannerRightEdge;
          });
        });
 
        setHighlightedWords(newHighlightedWords);
      };
 
      const animationFrame = requestAnimationFrame(function animate() {
        updateHighlightedWords();
        if (scanning) {
          requestAnimationFrame(animate);
        }
      });
 
      return () => cancelAnimationFrame(animationFrame);
    }
  }, [scanning, highlightWords]);
 
  const highlightText = (text: string) => {
    let result = text;
    highlightWords.forEach((phrase) => {
      const regex = new RegExp(`(${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
      result = result.replace(
        regex,
        (match) =>
          `<span class="highlight ${highlightedWords.includes(phrase) ? "active" : ""}" data-phrase="${phrase}">${match}</span>`,
      );
    });
    return result;
  };
 
  const renderAiProbability = (probability: number) => {
    const digits = probability.toString().padStart(2, "0").split("").map(Number);
 
    const digitVariants = {
      initial: { y: 0 },
      animate: {
        y: [0, -30, 0],
        transition: {
          repeat: Infinity,
          repeatType: "loop" as const,
          duration: 1.5,
          ease: "easeInOut",
        },
      },
    };
 
    return (
      <>
        <div className="inline-flex items-center">
          <div className="inline-flex h-8 overflow-hidden">
            {digits.map((digit, index) => (
              <motion.div
                key={`${index}-${digit}`}
                variants={digitVariants}
                initial="initial"
                animate="animate"
                className="inline-flex h-8 w-6 flex-col items-center justify-center"
              >
                {[digit, (digit + 1) % 10, (digit + 2) % 10].map((n, i) => (
                  <span key={i} className="font-bold leading-8 text-purple-900">
                    {n}
                  </span>
                ))}
              </motion.div>
            ))}
          </div>
        </div>
      </>
    );
  };
 
  return (
    <div className="relative mx-auto w-full max-w-2xl rounded-lg bg-white p-14 shadow-md">
      <div className="pb-5 text-center">
        <p className="p-5 text-2xl font-bold">Free AI Content Detector</p>
        <p className="pb-8">Brand new content in seconds. Remove any form of plagiarism</p>
      </div>
 
      <motion.div
        ref={containerRef}
        className="relative overflow-hidden rounded bg-white p-4 shadow-lg"
        style={{ minHeight: "120px" }}
        initial={{ y: 100, opacity: 0 }}
        animate={{ y: 0, opacity: 1 }}
        transition={{ duration: 0.4, ease: "easeOut" }}
      >
        <div
          ref={contentRef}
          className="relative"
          dangerouslySetInnerHTML={{ __html: highlightText(content) }}
          style={{ color: "#666" }}
        />
        <motion.div
          ref={scannerRef}
          className="pointer-events-none absolute -top-5 left-0 h-[calc(100%+40px)]"
          initial={{ x: "-87%" }}
          animate={scannerAnimation}
        >
          <div className="flex h-full flex-row-reverse">
            <div className="h-full w-1.5 bg-[#887FF2]" />
            <div className="h-full w-24 bg-custom-gradient" />
          </div>
        </motion.div>
      </motion.div>
 
      <div className="rounded">
        <div className="flex justify-center">
          <button
            onClick={startScanning}
            className="mt-4 rounded bg-[#887FF2] px-4 py-2 text-white"
            disabled={scanning}
          >
            {scanning ? "Scanning..." : "Start Scan"}
          </button>
        </div>
        <div className="relative mt-2 overflow-hidden text-center text-sm text-black">
          <div className="flex items-center justify-center">
            {aiProbability > 0 && renderAiProbability(Math.floor(aiProbability))}
            <span className="ml-1 font-bold text-purple-900">%</span>
            <span className="ml-1">AI Content Probability</span>
          </div>
        </div>
      </div>
 
      <style>{`
        .highlight {
          transition: background-color 0.3s ease;
          box-decoration-break: clone;
          -webkit-box-decoration-break: clone;
        }
        .highlight.active {
          background-color: #DAD9FE;
        }
        .scanned-text {
          color: #4B0082;
        }
      `}</style>
    </div>
  );
};
 
export default ContentScanner;

Credits

Built by Bandhan Majumder