Interactive SEO Card Animation

1import React, { useEffect, useRef, useState } from 'react'
2import { motion } from "framer-motion"
3import { Search } from 'lucide-react'
4
5const TYPING_TEXT = 'Your website in your location';
6
7const originalResults = [
8 { url: 'www.abc.com', title: 'Suggest tools for marketing', desc: <>I launched <span className="font-semibold">your website</span>. Can you suggest tools for marketing?</> },
9 { url: 'www.xyzwebsite.com', title: 'Good supplier', desc: <><span className="font-semibold">Your website</span> is a reliable supplier.</> },
10 { url: 'www.somesite.com', title: 'Different suppliers', desc: <>They provide different customizations, including <span className="font-semibold">your website</span>.</> },
11];
12
13export default function SeoCard() {
14 const [isHovered, setIsHovered] = useState(false);
15 const [typedText, setTypedText] = useState("");
16 const [results, setResults] = useState(originalResults);
17
18 const typingRef = useRef<NodeJS.Timeout | null>(null);
19 const resultRef = useRef<NodeJS.Timeout | null>(null);
20 const hoverTimeout = useRef<NodeJS.Timeout | null>(null);
21
22 const handleHoverStart = () => {
23 hoverTimeout.current = setTimeout(() => {
24 setIsHovered(true);
25 }, 100);
26 };
27
28 const handleHoverEnd = () => {
29 if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
30 setIsHovered(false);
31 };
32
33 useEffect(() => {
34 if (!isHovered) {
35 setTypedText('');
36 setResults(originalResults);
37
38 if (typingRef.current) clearInterval(typingRef.current);
39 if (resultRef.current) clearInterval(resultRef.current);
40 return;
41 }
42
43 let i = 0;
44 let j = 0;
45
46 const runAnimation = () => {
47 setTypedText('');
48 setResults([]);
49
50 i = 0;
51 j = 0;
52
53 const type = () => {
54 if (i <= TYPING_TEXT.length) {
55 setTypedText(TYPING_TEXT.slice(0, i));
56 i++;
57 typingRef.current = setTimeout(type, 40);
58 } else {
59 showResults();
60 }
61 };
62
63 const showResults = () => {
64 if (j < originalResults.length) {
65 const item = originalResults[j];
66
67 if (item) {
68 setResults(prev => [...prev, item]);
69 }
70
71 j++;
72 resultRef.current = setTimeout(showResults, 600);
73 } else {
74 resultRef.current = setTimeout(() => {
75 if (isHovered) runAnimation();
76 }, 2000);
77 }
78 };
79
80 type();
81 };
82
83 runAnimation();
84
85 return () => {
86 if (typingRef.current) clearTimeout(typingRef.current);
87 if (resultRef.current) clearTimeout(resultRef.current);
88 };
89 }, [isHovered]);
90
91 return (
92 <motion.div
93 onHoverStart={handleHoverStart}
94 onHoverEnd={handleHoverEnd}
95 className="p-6 row-span-2 flex flex-col gap-2 text-left rounded-xl ring-1 ring-gray-300 shadow-sm shadow-gray-200 hover:shadow-gray-300 hover:shadow-md transition group">
96 <div>
97 <p className="text-gray-700 leading-relaxed font-[600] pb-4"> Get found on google </p>
98 <p className="text-sm text-gray-600">We'll make sure your website is optimized for search engines, so you can get the traffic you're looking for.</p>
99 </div>
100 <div className="text-gray-500 flex flex-col h-full">
101 <div className="ring-1 ring-gray-300 shadow-sm shadow-gray-300 px-2 rounded-2xl mb-5 py-0.5 flex justify-between items-center gap-1 text-sm">
102 <div className='flex items-center gap-2 w-full'>
103 <svg viewBox="-3 0 262 262" xmlns="http://www.w3.org/2000/svg" width={12} preserveAspectRatio="xMidYMid" fill="#000000"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" fill="#4285F4"></path><path d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" fill="#34A853"></path><path d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" fill="#FBBC05"></path><path d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" fill="#EB4335"></path></g></svg>
104 <input value={typedText} readOnly placeholder={TYPING_TEXT} className="placeholder:text-xs placeholder:font-medium text-xs font-medium w-full" />
105 </div>
106 <Search width={12} />
107 </div>
108 <div className="pl-3 flex flex-col gap-3 min-h-56">
109 {results.map((r, i) => {
110 if (!r) return null;
111
112 return (
113 <motion.div
114 key={i}
115 initial={{ opacity: 0, y: 10 }}
116 animate={{ opacity: 1, y: 0 }}
117 transition={{ delay: i * 0.4, duration: 0.3 }}
118 >
119 <p className="text-xs">
120 {r.url}
121 </p>
122 <p className="text-[15px]">
123 {r.title}
124 </p>
125 <p className="text-xs"> {r.desc}
126 </p>
127 </motion.div>
128 )
129 })
130 } </div> <div className="pt-4">
131 </div>
132 <p className="text-sm mt-auto">
133 We continuously monitor search visibility and track your rankings across Google and Bing to ensure consistent growth.
134 </p>
135 </div>
136 </motion.div>
137 )
138}
139
140
Animated SEO Card Illustration | Interactive Search UI Component