apex-ui-kit 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/Licence.md +21 -0
  2. package/ReadMe.md +75 -0
  3. package/dist/ApexUI-Kit/templates/Avatar/Avatar.jsx +81 -0
  4. package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +101 -0
  5. package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +60 -0
  6. package/dist/ApexUI-Kit/templates/Carousel/Carousel.jsx +95 -0
  7. package/dist/ApexUI-Kit/templates/Carousel/DragCarousel.jsx +105 -0
  8. package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +161 -0
  9. package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +105 -0
  10. package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +297 -0
  11. package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +125 -0
  12. package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +146 -0
  13. package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +111 -0
  14. package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +371 -0
  15. package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +183 -0
  16. package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +249 -0
  17. package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +268 -0
  18. package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +155 -0
  19. package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +114 -0
  20. package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +153 -0
  21. package/dist/ApexUI-Kit/templates/OrbsBackground/FloatingOrbs.jsx +96 -0
  22. package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +127 -0
  23. package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +161 -0
  24. package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +107 -0
  25. package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +126 -0
  26. package/dist/ApexUI-Kit/templates/RainBackground/RainBackground.jsx +109 -0
  27. package/dist/ApexUI-Kit/templates/RippleBackground/RippleBackground.jsx +160 -0
  28. package/dist/ApexUI-Kit/templates/ScrollMarquee/ScrollMarquee.jsx +174 -0
  29. package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +241 -0
  30. package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +216 -0
  31. package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +117 -0
  32. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +252 -0
  33. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeConfig.js +8 -0
  34. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +560 -0
  35. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +118 -0
  36. package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +85 -0
  37. package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +151 -0
  38. package/dist/cli/index.d.ts +2 -0
  39. package/dist/cli/index.js +145 -0
  40. package/package.json +56 -0
@@ -0,0 +1,107 @@
1
+ import React, { useRef, useEffect } from "react";
2
+ import { gsap } from "gsap";
3
+ import { BsTwitterX } from "react-icons/bs";
4
+ // --- SVG Icons (No changes here) --- //
5
+ const GitHubIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.5.5.09.66-.22.66-.48 0-.24-.01-.87-.01-1.7-2.78.6-3.37-1.34-3.37-1.34-.45-1.15-1.1-1.46-1.1-1.46-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.89 1.52 2.34 1.08 2.91.83.09-.65.35-1.08.63-1.33-2.22-.25-4.56-1.11-4.56-4.95 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02A9.56 9.56 0 0112 6.8c.85.004 1.71.12 2.51.35 1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.85-2.34 4.7-4.57 4.95.36.31.68.92.68 1.85 0 1.33-.01 2.4-.01 2.73 0 .27.16.58.67.48A10.01 10.01 0 0022 12c0-5.52-4.48-10-10-10z" /></svg>);
6
+ const LinkedInIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" /><rect x="2" y="9" width="4" height="12" /><circle cx="4" cy="4" r="2" /></svg>);
7
+ const TwitterIcon = () => (<BsTwitterX size={24} />);
8
+ const InstagramIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="5" ry="5" /><path d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37z" /><line x1="17.5" y1="6.5" x2="17.5" y2="6.5" /></svg>);
9
+ const ParallaxProfileCard = ({ profile }) => {
10
+ const defaultProfile = {
11
+ avatarUrl: "/assets/spiderman.png",
12
+ name: "Spider Man",
13
+ username: "spider_man",
14
+ dob: "10-08-2001",
15
+ title: "PhotoGrapher",
16
+ status: "Available for Hire",
17
+ socialLinks: {
18
+ github: "https://github.com/",
19
+ linkedin: "https://www.linkedin.com/",
20
+ twitter: "https://twitter.com/",
21
+ instagram: "https://www.instagram.com/",
22
+ },
23
+ // Button actions
24
+ onHireClick: () => alert("Hire Me button clicked!"),
25
+ onCvClick: () => alert("CV button clicked!"),
26
+ };
27
+
28
+ const {
29
+ avatarUrl, name, username, dob, title, status, socialLinks, onHireClick, onCvClick
30
+ } = profile || defaultProfile;
31
+
32
+ const cardRef = useRef(null);
33
+
34
+ useEffect(() => {
35
+ const card = cardRef.current;
36
+ if (!card) return;
37
+ const handleMouseMove = (e) => {
38
+ const { left, top, width, height } = card.getBoundingClientRect();
39
+ const x = e.clientX - left; const y = e.clientY - top;
40
+ const rotateX = gsap.utils.mapRange(0, height, -9, 9, y);
41
+ const rotateY = gsap.utils.mapRange(0, width, 9, -9, x);
42
+ gsap.to(card, { duration: 0.7, rotationX: rotateX, rotationY: rotateY, scale: 1.05, ease: "power3.out" });
43
+ };
44
+ const handleMouseLeave = () => {
45
+ gsap.to(card, { duration: 1, rotationX: 0, rotationY: 0, scale: 1, ease: "elastic.out(1, 0.3)" });
46
+ };
47
+ gsap.set(card, { transformPerspective: 1000 });
48
+ card.addEventListener("mousemove", handleMouseMove);
49
+ card.addEventListener("mouseleave", handleMouseLeave);
50
+ return () => {
51
+ card.removeEventListener("mousemove", handleMouseMove);
52
+ card.removeEventListener("mouseleave", handleMouseLeave);
53
+ };
54
+ }, []);
55
+
56
+ return (
57
+ <div
58
+ ref={cardRef}
59
+ className="relative w-[300px] h-[440px] bg-[#1D2B3A] rounded-3xl p-6 border border-cyan-400/30 shadow-2xl shadow-cyan-900/40 flex flex-col"
60
+ style={{ transformStyle: "preserve-3d" }}
61
+ >
62
+ <div className="absolute inset-0 bg-gradient-to-br from-[#1D2B3A] to-[#111827] rounded-3xl"></div>
63
+ <div className="absolute top-0 left-0 w-full h-full" style={{ background: "radial-gradient(circle at 50% 50%, rgba(34, 211, 238, 0.08), transparent 70%)", transform: "translateZ(20px)" }}></div>
64
+
65
+ <div className="relative z-10 flex flex-col items-center justify-between h-full" style={{ transform: "translateZ(50px)" }}>
66
+ {/* Top Part */}
67
+ <div className="text-center">
68
+ <img src={avatarUrl} alt="Profile" className="w-28 h-28 rounded-full mx-auto mb-3 border-4 border-cyan-400 shadow-lg object-cover" onError={(e) => { e.target.onerror = null; e.target.src = "https://placehold.co/128x128/1D2B3A/A5F3FC?text=ERR"; }} />
69
+ <h2 className="text-2xl font-bold text-cyan-300 tracking-wide">{name}</h2>
70
+ <div className="flex flex-col items-center mt-1">
71
+ <span className="text-xs font-semibold text-slate-300 bg-slate-700 px-2 py-0.5 rounded-full mb-1">@{username}</span>
72
+ <span className="text-xs font-medium text-cyan-400 bg-slate-700 px-2 py-0.5 rounded-full">DOB: {dob}</span>
73
+ </div>
74
+ </div>
75
+
76
+ {/* Bottom Part */}
77
+ <div className="w-full text-center">
78
+ <p className="text-slate-400 text-lg">{title}</p>
79
+ {status && (
80
+ <div className="flex items-center justify-center gap-2 mt-2">
81
+ <span className="relative flex h-3 w-3"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span><span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span></span>
82
+ <span className="text-green-300 text-sm">{status}</span>
83
+ </div>
84
+ )}
85
+ <div className="flex justify-center gap-4 my-4">
86
+ {socialLinks?.github && (<a href={socialLinks.github} target="_blank" rel="noopener noreferrer" title="GitHub" className="text-cyan-400 hover:text-white transition-colors duration-200"><GitHubIcon /></a>)}
87
+ {socialLinks?.linkedin && (<a href={socialLinks.linkedin} target="_blank" rel="noopener noreferrer" title="LinkedIn" className="text-cyan-400 hover:text-white transition-colors duration-200"><LinkedInIcon /></a>)}
88
+ {socialLinks?.twitter && (<a href={socialLinks.twitter} target="_blank" rel="noopener noreferrer" title="Twitter" className="text-cyan-400 hover:text-white transition-colors duration-200"><TwitterIcon /></a>)}
89
+ {socialLinks?.instagram && (<a href={socialLinks.instagram} target="_blank" rel="noopener noreferrer" title="Instagram" className="text-cyan-400 hover:text-white transition-colors duration-200"><InstagramIcon /></a>)}
90
+ </div>
91
+
92
+ {/* Buttons Section */}
93
+ <div className="flex justify-center gap-3">
94
+ <button onClick={onHireClick} className="w-1/2 bg-cyan-500 text-slate-900 font-bold py-2 px-4 rounded-lg hover:bg-cyan-400 transition-colors duration-300">
95
+ Hire Me
96
+ </button>
97
+ <button onClick={onCvClick} className="w-1/2 bg-slate-700 text-cyan-300 font-bold py-2 px-4 rounded-lg hover:bg-slate-600 transition-colors duration-300">
98
+ CV
99
+ </button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ );
105
+ };
106
+
107
+ export default ParallaxProfileCard;
@@ -0,0 +1,126 @@
1
+ import React, { useRef, useEffect, useState } from "react";
2
+
3
+ const people = [
4
+ { id: 1, name: "Captain America", job: "Leader of the Avengers", img: "/assets/captainamerica.png" },
5
+ { id: 2, name: "Doctor Strange", job: "Sorcerer Supreme", img: "/assets/doctorStrange.png" },
6
+ { id: 3, name: "Iron Man", job: "Leader Of Stark Industries", img: "/assets/ironman.png" },
7
+ { id: 4, name: "HULK", job: "Scientist", img: "/assets/hulk.png" },
8
+ { id: 5, name: "Spider-Man", job: "Friendly Neighborhood Spider-Man", img: "/assets/spiderman.png" },
9
+ { id: 6, name: "Thanos", job: "The Mad Titan", img: "/assets/thanos.png" },
10
+ ];
11
+
12
+ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "#fff", badgeTextColor = "#212121" }) => {
13
+ const containerRef = useRef(null);
14
+ const [pos, setPos] = useState({ x: 0, y: 0 });
15
+ const [hovered, setHovered] = useState(false);
16
+ const [activeIndex, setActiveIndex] = useState(0);
17
+
18
+ // 🛠️ Auto cycle with sync fix
19
+ useEffect(() => {
20
+ if (hovered) return;
21
+
22
+ const autoInterval = setInterval(() => {
23
+ setActiveIndex((prev) => (prev + 1) % people.length);
24
+ }, interval);
25
+
26
+ return () => clearInterval(autoInterval);
27
+ }, [hovered, interval]);
28
+
29
+ // 🖱️ Mouse tracking
30
+ useEffect(() => {
31
+ const container = containerRef.current;
32
+ if (!container) return;
33
+
34
+ const handleMove = (e) => {
35
+ const rect = container.getBoundingClientRect();
36
+ setPos({
37
+ x: e.clientX - rect.left,
38
+ y: e.clientY - rect.top,
39
+ });
40
+ setHovered(true);
41
+ };
42
+
43
+ const handleLeave = () => {
44
+ setHovered(false);
45
+ };
46
+
47
+ container.addEventListener("pointermove", handleMove);
48
+ container.addEventListener("pointerleave", handleLeave);
49
+
50
+ return () => {
51
+ container.removeEventListener("pointermove", handleMove);
52
+ container.removeEventListener("pointerleave", handleLeave);
53
+ };
54
+ }, []);
55
+
56
+ const person = people[activeIndex]; // ✅ SYNCED HERE
57
+
58
+ return (
59
+ <div className="relative w-full h-fit flex items-center justify-center">
60
+ <div ref={containerRef} className="absolute flex items-center justify-center w-64 h-66 cursor-none">
61
+ {/* 🔁 Image stack with auto fade */}
62
+ {people.map((p, i) => (
63
+ <img
64
+ key={i}
65
+ src={p.img}
66
+ alt={p.name}
67
+ className={`absolute w-full h-full object-cover rounded-xl transition-opacity duration-1000 ease-in-out border border-white/10`}
68
+ style={{
69
+ transform: `translate(${i * -2}px, ${i * -2}px)`,
70
+ zIndex: people.length - i,
71
+ opacity: activeIndex === i ? 1 : 0,
72
+ }}
73
+ />
74
+ ))}
75
+ {/* 🖱️ Custom SVG Cursor */}
76
+ {hovered && (
77
+ <>
78
+ <div
79
+ className="absolute z-50"
80
+ style={{
81
+ left: pos.x,
82
+ top: pos.y,
83
+ transform: "translate(-50%, -50%)",
84
+ pointerEvents: "none",
85
+ }}
86
+ >
87
+ <svg
88
+ xmlns="http://www.w3.org/2000/svg"
89
+ width="28"
90
+ height="28"
91
+ viewBox="0 0 24 24"
92
+ >
93
+ <path
94
+ fill={cursorColor}
95
+ stroke="#000"
96
+ strokeWidth="1.8"
97
+ d="M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87a.5.5 0 0 0 .35-.85L6.35 2.85a.5.5 0 0 0-.85.35Z"
98
+ />
99
+ </svg>
100
+ </div>
101
+
102
+ {/* 🎯 Job Passion Label */}
103
+ <div
104
+ className="absolute z-40"
105
+ style={{
106
+ left: pos.x + 24,
107
+ top: pos.y + 36,
108
+ transform: "translate(-50%, -50%)",
109
+ pointerEvents: "none",
110
+ color: badgeTextColor,
111
+ }}
112
+ >
113
+ <div className="px-4 py-3 rounded-full text-[11px] overflow-hidden font-semibold shadow-[0_2px_8px_rgba(0,0,0,0.25)] border border-black/10" style={{ backgroundColor: badgeColor }}>
114
+ <span className="text-[12px] font-medium tracking-wide whitespace-nowrap drop-shadow" style={{ color: badgeTextColor }}>
115
+ {person.name} — {person.job}
116
+ </span>
117
+ </div>
118
+ </div>
119
+ </>
120
+ )}
121
+ </div>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export default PointerFollower;
@@ -0,0 +1,109 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import gsap from 'gsap';
3
+ function createRainEffectWithCollision(container, dropCount = 0, dropColor, collisionColor, dropGradient, collisionGradient) {
4
+ if (!container) return;
5
+ for (let i = 0; i < dropCount; i++) {
6
+ const drop = document.createElement('div');
7
+ drop.className = 'rain-drop pointer-events-none';
8
+ drop.style.position = 'absolute';
9
+ drop.style.left = Math.random() * 98 + '%';
10
+ drop.style.top = Math.random() * -100 - 20 + 'px';
11
+ drop.style.width = '2px';
12
+ drop.style.height = Math.random() * 32 + 16 + 'px';
13
+ drop.style.background = dropGradient ? dropGradient : `linear-gradient(to bottom, ${dropColor} 60%, ${collisionColor} 100%)`;
14
+ drop.style.opacity = Math.random() * 0.3 + 0.3;
15
+ drop.style.borderRadius = '1px';
16
+ drop.style.zIndex = 1;
17
+ container.appendChild(drop);
18
+ animateRainDropWithCollision(drop, container, collisionColor, dropGradient, collisionGradient, dropColor);
19
+ }
20
+ }
21
+
22
+ function animateRainDropWithCollision(drop, container, collisionColor, dropGradient, collisionGradient, dropColor) {
23
+ const duration = Math.random() * 1.2 + 0.8;
24
+ gsap.fromTo(drop, {
25
+ y: 0,
26
+ }, {
27
+ y: container.offsetHeight + 40,
28
+
29
+ duration,
30
+ ease: 'power1.in',
31
+ onUpdate: function () {
32
+ const rect = drop.getBoundingClientRect();
33
+ const parentRect = container.getBoundingClientRect();
34
+ if (!drop._collided && rect.bottom >= parentRect.bottom - 8) {
35
+ drop._collided = true;
36
+ drop.style.background = collisionGradient ? collisionGradient : collisionColor;
37
+ const relX = rect.left - parentRect.left + rect.width / 2;
38
+ createRainCollisionExplosion(relX, container.offsetHeight - 8, container, collisionColor, collisionGradient);
39
+ }
40
+ },
41
+ onStart: function () {
42
+ drop._collided = false;
43
+ drop.style.background = dropGradient ? dropGradient : `linear-gradient(to bottom, ${dropColor} 60%, ${collisionColor} 100%)`;
44
+ },
45
+ onComplete: () => {
46
+ drop.style.left = Math.random() * 98 + '%';
47
+ drop.style.height = Math.random() * 32 + 16 + 'px';
48
+ drop.style.opacity = Math.random() * 0.3 + 0.3;
49
+ drop.style.background = dropGradient ? dropGradient : `linear-gradient(to bottom, ${dropColor} 60%, ${collisionColor} 100%)`;
50
+ gsap.set(drop, { y: 0, opacity: drop.style.opacity });
51
+ animateRainDropWithCollision(drop, container, collisionColor, dropGradient, collisionGradient, dropColor);
52
+ }
53
+ });
54
+ }
55
+
56
+ function createRainCollisionExplosion(x, y, container, collisionColor = '#e0e7ff', collisionGradient) {
57
+ const explosion = document.createElement("div");
58
+ explosion.className = "absolute z-50 h-2 w-2 pointer-events-none";
59
+ explosion.style.left = `${x}px`;
60
+ explosion.style.top = `${y}px`;
61
+ explosion.style.transform = "translate(-50%, 0)";
62
+ const blurBar = document.createElement("div");
63
+ blurBar.style.position = "absolute";
64
+ blurBar.style.left = "-0.7rem";
65
+ blurBar.style.top = "0";
66
+ blurBar.style.width = "1.4rem";
67
+ blurBar.style.height = "0.18rem";
68
+ blurBar.style.borderRadius = "1rem";
69
+ blurBar.style.background = collisionGradient ? collisionGradient : `${collisionColor}`;
70
+ blurBar.style.filter = "blur(2px)";
71
+ explosion.appendChild(blurBar);
72
+ for (let i = 0; i < 6; i++) {
73
+ const span = document.createElement("span");
74
+ span.style.position = "absolute";
75
+ span.style.height = "0.18rem";
76
+ span.style.width = "0.18rem";
77
+ span.style.borderRadius = "50%";
78
+ span.style.background = collisionGradient ? collisionGradient : `${collisionColor}`;
79
+ explosion.appendChild(span);
80
+ const dx = Math.floor(Math.random() * 16 - 8);
81
+ const dy = Math.floor(Math.random() * -10 - 2);
82
+ gsap.fromTo(span, { x: 0, y: 0 }, {
83
+ x: dx,
84
+ y: dy,
85
+ opacity: 0,
86
+ duration: Math.random() * 0.5 + 0.2,
87
+ ease: "power2.out",
88
+ onComplete: () => { span.remove(); }
89
+ });
90
+ }
91
+ gsap.fromTo(blurBar, { opacity: 0 }, { opacity: 1, duration: 0.12, yoyo: true, repeat: 1, onComplete: () => blurBar.remove() });
92
+ setTimeout(() => explosion.remove(), 500);
93
+ container.appendChild(explosion);
94
+ }
95
+
96
+ const RainBackground = ({ dropCount = 32, dropColor = '#fff', collisionColor = '#e0e7ff', dropGradient, collisionGradient }) => {
97
+ const rainRef = useRef(null);
98
+ useEffect(() => {
99
+ const container = rainRef.current;
100
+ if (!container) return;
101
+ createRainEffectWithCollision(container, dropCount, dropColor, collisionColor, dropGradient, collisionGradient);
102
+ return () => { container.innerHTML = ''; };
103
+ }, [dropCount, dropColor, collisionColor, dropGradient, collisionGradient]);
104
+ return (
105
+ <div ref={rainRef} id="beams-collision-container" className="absolute inset-0 w-full h-full pointer-events-none z-0" style={{ overflow: 'hidden' }} />
106
+ );
107
+ };
108
+
109
+ export default RainBackground;
@@ -0,0 +1,160 @@
1
+ import React, { useLayoutEffect, useRef, useState, useEffect } from 'react';
2
+ import { gsap } from 'gsap';
3
+
4
+ // --- RippleBackground Component (Optimized for Mobile) ---
5
+ const RippleBackground = ({
6
+ children,
7
+ className,
8
+ containerClassName,
9
+ colors,
10
+ desktopGridSize = 20, // Grid size for desktop
11
+ mobileGridSize = 10, // Grid size for mobile
12
+ }) => {
13
+ const containerRef = useRef(null);
14
+ const shapesRef = useRef([]);
15
+ const [gridSize, setGridSize] = useState(desktopGridSize);
16
+ const [isMobile, setIsMobile] = useState(false);
17
+
18
+ // Check screen size to determine if it's mobile
19
+ useEffect(() => {
20
+ const checkScreenSize = () => {
21
+ const isMobileDevice = window.innerWidth < 768;
22
+ setIsMobile(isMobileDevice);
23
+ setGridSize(isMobileDevice ? mobileGridSize : desktopGridSize);
24
+ };
25
+
26
+ checkScreenSize();
27
+ window.addEventListener('resize', checkScreenSize);
28
+ return () => window.removeEventListener('resize', checkScreenSize);
29
+ }, [mobileGridSize, desktopGridSize]);
30
+
31
+
32
+ const shapeColors = colors ?? [
33
+ "#10b981", // emerald-500
34
+ "#06b6d4", // cyan-500
35
+ "#6366f1", // indigo-500
36
+ "#a855f7", // purple-500
37
+ ];
38
+
39
+ // Handles the ripple effect on click
40
+ const handleClick = (e) => {
41
+ const { clientX, clientY } = e;
42
+ const shapes = shapesRef.current;
43
+
44
+ shapes.forEach(shape => {
45
+ if (!shape) return;
46
+ const rect = shape.getBoundingClientRect();
47
+ const dx = rect.left + rect.width / 2 - clientX;
48
+ const dy = rect.top + rect.height / 2 - clientY;
49
+ const distance = Math.sqrt(dx * dx + dy * dy);
50
+ const delay = distance * 0.005; // Ripple delay based on distance
51
+
52
+ gsap.timeline({ defaults: { overwrite: 'auto' } })
53
+ .to(shape, {
54
+ scale: 2.5,
55
+ opacity: 1,
56
+ duration: 0.3,
57
+ ease: "power2.out",
58
+ delay: delay
59
+ })
60
+ .to(shape, {
61
+ scale: 1,
62
+ opacity: 0.5,
63
+ duration: 0.5,
64
+ ease: "power2.in"
65
+ });
66
+ });
67
+ };
68
+
69
+ // Handles micro-interaction on hover
70
+ const handleMouseEnter = (shape) => {
71
+ gsap.to(shape, {
72
+ scale: 2,
73
+ opacity: 1,
74
+ duration: 0.3,
75
+ ease: 'power2.out',
76
+ overwrite: 'auto'
77
+ });
78
+ };
79
+
80
+ const handleMouseLeave = (shape) => {
81
+ // Returns the shape to its normal blinking state
82
+ gsap.to(shape, {
83
+ scale: 1,
84
+ opacity: 0.3,
85
+ duration: 2,
86
+ ease: 'power2.out',
87
+ overwrite: 'auto'
88
+ });
89
+ };
90
+
91
+ // Continuous blinking animation
92
+ useLayoutEffect(() => {
93
+ const shapes = shapesRef.current.filter(s => s);
94
+ if (shapes.length === 0) return;
95
+
96
+ // On mobile, we disable the heavy continuous animation for better performance
97
+ if (isMobile) {
98
+ // Set a default static state for mobile
99
+ gsap.set(shapes, { scale: 1, opacity: 0.3 });
100
+ return;
101
+ }
102
+
103
+ const masterTl = gsap.timeline({ repeat: -1, yoyo: true });
104
+
105
+ shapes.forEach(shape => {
106
+ masterTl.to(shape, {
107
+ scale: () => Math.random() * 1.5 + 0.5,
108
+ opacity: () => Math.random() * 0.7 + 0.3,
109
+ duration: 2,
110
+ ease: "sine.inOut",
111
+ }, Math.random() * 2); // Random start time for each shape
112
+ });
113
+
114
+ return () => {
115
+ masterTl.kill(); // Cleanup the timeline
116
+ };
117
+ }, [gridSize, isMobile]); // Rerun effect if grid size or mobile status changes
118
+
119
+ return (
120
+ <div
121
+ className={`relative h-[60vh] lg:h-[80vh] w-screen flex flex-col items-center justify-center overflow-hidden ${containerClassName}`}
122
+ onClick={handleClick} // Click event listener on the main container
123
+ >
124
+ {/* Shapes Grid Container */}
125
+ <div
126
+ ref={containerRef}
127
+ className="absolute inset-0 z-0 w-full h-full grid"
128
+ style={{
129
+ gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
130
+ gridTemplateRows: `repeat(${gridSize}, 1fr)`,
131
+ }}
132
+ >
133
+ {Array.from({ length: gridSize * gridSize }).map((_, index) => (
134
+ <div
135
+ key={index}
136
+ className="w-full h-full flex items-center justify-center"
137
+ onMouseEnter={() => handleMouseEnter(shapesRef.current[index])}
138
+ onMouseLeave={() => handleMouseLeave(shapesRef.current[index])}
139
+ >
140
+ <div
141
+ ref={el => shapesRef.current[index] = el}
142
+ className="w-2 h-2 rounded-full pointer-events-none"
143
+ style={{
144
+ backgroundColor: shapeColors[index % shapeColors.length],
145
+ // Performance optimization for animations
146
+ willChange: 'transform, opacity',
147
+ }}
148
+ />
149
+ </div>
150
+ ))}
151
+ </div>
152
+
153
+ <div className={`relative z-10 ${className}`}>
154
+ {children}
155
+ </div>
156
+ </div>
157
+ );
158
+ };
159
+
160
+ export default RippleBackground;
@@ -0,0 +1,174 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { useRef, useLayoutEffect, useState } from "react";
3
+ import { motion, useTransform, useMotionValue, useAnimationFrame, useScroll, useVelocity, useSpring } from "framer-motion";
4
+
5
+ function useElementWidth(ref) {
6
+ const [width, setWidth] = useState(0);
7
+
8
+ useLayoutEffect(() => {
9
+ function updateWidth() {
10
+ if (ref.current) {
11
+ setWidth(ref.current.offsetWidth);
12
+ }
13
+ }
14
+ updateWidth();
15
+ window.addEventListener("resize", updateWidth);
16
+ return () => window.removeEventListener("resize", updateWidth);
17
+ }, [ref]);
18
+
19
+ return width;
20
+ }
21
+
22
+ // Utility for seamless wrap
23
+ function wrapValue(min, max, v) {
24
+ const range = max - min;
25
+ const mod = (((v - min) % range) + range) % range;
26
+ return mod + min;
27
+ }
28
+
29
+ // Main Marquee Component
30
+ export function ScrollMarquee({
31
+ items = ['ApexUI', 'apexui'],
32
+ speed = 50,
33
+ direction,
34
+ className = "apexui-marquee-text",
35
+ repeat = 10,
36
+ containerClass = "apexui-marquee-container",
37
+ scrollerClass = "apexui-marquee-track",
38
+ containerStyle,
39
+ scrollerStyle,
40
+ textStroke = false,
41
+ textStrokeColor = '#C27AFF',
42
+ textFillColor = '#1E2637',
43
+ textColor = '#fff',
44
+ scrollRef,
45
+ }) {
46
+ // Single scrolling row
47
+ function MarqueeRow({ children, baseSpeed = speed, direction = "left" }) {
48
+ const baseX = useMotionValue(0);
49
+ const copyRef = useRef(null);
50
+ const copyWidth = useElementWidth(copyRef);
51
+
52
+ // Scroll tracking (window or custom container)
53
+ const scrollOptions = scrollRef ? { container: scrollRef } : {};
54
+ const { scrollY } = useScroll(scrollOptions);
55
+ const scrollVelocity = useVelocity(scrollY);
56
+ const smoothVelocity = useSpring(scrollVelocity, { damping: 50, stiffness: 400 });
57
+ const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 2], { clamp: false });
58
+
59
+ const x = useTransform(baseX, (v) => {
60
+ if (copyWidth === 0) return "0px";
61
+ return `${wrapValue(-copyWidth, 0, v)}px`;
62
+ });
63
+
64
+ // Unique scroll-based animation: gradient appears on scroll, white by default
65
+ // Animate gradient opacity and hue-rotate based on velocity
66
+ const hue = useTransform(velocityFactor, v => Math.max(-40, Math.min(40, v * 30)));
67
+ const gradientOpacity = useTransform(velocityFactor, v => Math.min(1, Math.abs(v) * 0.7));
68
+ const filter = useTransform(hue, h => `hue-rotate(${h}deg)`);
69
+
70
+ // Alternate direction, flip on scroll
71
+ const directionFactor = useRef(direction === "left" ? -1 : 1);
72
+ const lastVelocity = useRef(0);
73
+ useAnimationFrame((t, delta) => {
74
+ const v = velocityFactor.get();
75
+ if (v !== 0 && Math.sign(v) !== Math.sign(lastVelocity.current)) {
76
+ directionFactor.current *= -1;
77
+ }
78
+ lastVelocity.current = v;
79
+ let moveBy = directionFactor.current * (baseSpeed + Math.abs(v * 100)) * (delta / 1000);
80
+ baseX.set(baseX.get() + moveBy);
81
+ });
82
+
83
+ // Modern font and gradient color, with color/stroke props
84
+ // Use Google font 'Barlow Condensed', fallback to 'Montserrat', then sans-serif
85
+ const spans = [];
86
+ for (let i = 0; i < repeat; i++) {
87
+ spans.push(
88
+ <span
89
+ className={`flex-shrink-0 ${className} tracking-tighter font-semibold custom-marquee-span`}
90
+ key={i}
91
+ ref={i === 0 ? copyRef : null}
92
+ style={{
93
+ fontFamily: "helvetica, Arial, sans-serif",
94
+ cursor: "default",
95
+ fontOpticalSizing: 'auto',
96
+ color: !textStroke ? textColor : '#fff',
97
+ WebkitTextStroke: undefined,
98
+ WebkitTextFillColor: undefined,
99
+ fontSize: '6.8rem',
100
+ transition: 'all 0.5s cubic-bezier(.4,2,.6,1)',
101
+ position: 'relative',
102
+ display: 'inline-block',
103
+ }}
104
+ >
105
+ <span style={{ position: 'relative', zIndex: 1 }}>{children}</span>
106
+ <motion.span
107
+ className={"marquee-gradient-overlay absolute left-0 top-0 w-full h-full bg-gradient-to-r from-fuchsia-400 via-cyan-400 to-lime-400 bg-clip-text text-transparent drop-shadow-lg"}
108
+ style={{
109
+ filter,
110
+ opacity: gradientOpacity,
111
+ WebkitTextFillColor: 'transparent',
112
+ mixBlendMode: 'normal',
113
+ zIndex: 2,
114
+ }}
115
+ aria-hidden="true"
116
+ >
117
+ {children}
118
+ </motion.span>
119
+ </span>
120
+ );
121
+ }
122
+
123
+ // Add hover effect via inline style and a custom class
124
+ // The hover will show a stroke and fill color
125
+ // Add a <style> tag for the custom class
126
+ // This is safe in component scope in React
127
+ return (
128
+ <div className={`${containerClass} relative overflow-hidden`} style={containerStyle}>
129
+ <style>{`
130
+ .custom-marquee-span {
131
+ transition: all 0.5s cubic-bezier(.4,2,.6,1);
132
+ }
133
+ .custom-marquee-span:hover {
134
+ -webkit-text-stroke: 2px ${textStrokeColor};
135
+ -webkit-text-fill-color: ${textFillColor};
136
+ color: ${textFillColor};
137
+ filter: drop-shadow(0 2px 12px ${textStrokeColor}55);
138
+ transition: all 0.5s cubic-bezier(.4,2,.6,1);
139
+ }
140
+ .custom-marquee-span:hover .marquee-gradient-overlay {
141
+ opacity: 0 !important;
142
+ }
143
+ `}</style>
144
+ <motion.div
145
+ className={`${scrollerClass} flex h-28 whitespace-nowrap text-center font-barlow text-5xl md:text-[5.5rem] tracking-[0.08em] md:leading-[6rem]`}
146
+ style={{ x, ...scrollerStyle }}
147
+ >
148
+ {spans}
149
+ </motion.div>
150
+ </div>
151
+ );
152
+ }
153
+
154
+ // Render all marquee rows
155
+ return (
156
+ <section>
157
+ {items.map((item, idx) => {
158
+ // Alternate direction: even left, odd right
159
+ const rowDirection = direction ? direction : (idx % 2 === 0 ? "left" : "right");
160
+ return (
161
+ <MarqueeRow
162
+ key={idx}
163
+ baseSpeed={speed}
164
+ direction={rowDirection}
165
+ >
166
+ {item}&nbsp;
167
+ </MarqueeRow>
168
+ );
169
+ })}
170
+ </section>
171
+ );
172
+ }
173
+
174
+ export default ScrollMarquee;