NEW
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
Learn more
Skip to content
Docs

Content Scan

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

Loading...

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/feature-cards/content-scan.json

Manual

Install dependencies

npm install motion

Add to your CSS

Add the following to your CSS file.

@theme {
  --bg-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 { motion, useAnimation } from "motion/react";
import type React from "react";
import { useEffect, useRef, useState } from "react";
 
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" as const,
        },
      },
    };
 
    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