apex-ui-kit 2.10.0 → 2.12.0

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.
@@ -1,120 +1,140 @@
1
- /* eslint-disable no-unused-vars */
2
- import React, { useState } from 'react';
3
- import { motion, AnimatePresence } from "framer-motion";
4
- import { ArrowLeft, ArrowRight } from 'lucide-react';
5
-
6
-
7
- // --- Testimonials Component ---
8
- const Testimonials = ({ testimonials }) => {
9
- const [activeIndex, setActiveIndex] = useState(0);
10
-
11
- const handleNext = () => {
12
- setActiveIndex((prevIndex) => (prevIndex + 1) % testimonials.length);
13
- };
14
-
15
- const handlePrev = () => {
16
- setActiveIndex((prevIndex) => (prevIndex - 1 + testimonials.length) % testimonials.length);
17
- };
18
-
19
- // Animation variants for the text content
20
- const textVariants = {
21
- enter: { opacity: 0, y: 20, filter: "blur(5px)" },
22
- center: { opacity: 1, y: 0, filter: "blur(0px)" },
23
- exit: { opacity: 0, y: -20, filter: "blur(5px)" },
24
- };
25
-
26
- return (
27
- <div className="w-full max-w-4xl mx-auto bg-slate-950 rounded-2xl p-6 sm:p-8 md:p-12 border border-slate-800 grid md:grid-cols-2 gap-8 md:gap-12 items-center overflow-hidden">
28
-
29
- {/* Left Part: Text Content and Navigation */}
30
- <div className="flex flex-col justify-center h-full text-center md:text-left order-2 md:order-1">
31
- {/* Title, subtitle and quote with animation; the min height keeps the buttons from jumping. */}
32
- <div className="relative min-h-[13rem] sm:min-h-[12rem]">
33
- <AnimatePresence initial={false} mode="wait">
34
- <motion.div
35
- key={activeIndex}
36
- variants={textVariants}
37
- initial="enter"
38
- animate="center"
39
- exit="exit"
40
- transition={{ duration: 0.4, ease: "easeInOut" }}
41
- className="w-full"
42
- >
43
- <h2 className="text-2xl sm:text-3xl font-bold text-white tracking-tight">{testimonials[activeIndex].author}</h2>
44
- <p className="mt-2 text-lime-400 font-medium">{testimonials[activeIndex].title}</p>
45
- <p className="mt-4 text-slate-400 text-sm leading-relaxed">"{testimonials[activeIndex].text}"</p>
46
- </motion.div>
47
- </AnimatePresence>
48
- </div>
49
-
50
- {/* Navigation Buttons with Microinteractions */}
51
- <div className="flex items-center justify-center md:justify-start gap-4 mt-8">
52
- <motion.button
53
- onClick={handlePrev}
54
- className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
55
- whileHover={{ scale: 1.1 }}
56
- whileTap={{ scale: 0.95 }}
57
- transition={{ type: 'spring', stiffness: 400, damping: 17 }}
58
- >
59
- <ArrowLeft size={20} />
60
- </motion.button>
61
- <motion.button
62
- onClick={handleNext}
63
- className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
64
- whileHover={{ scale: 1.1 }}
65
- whileTap={{ scale: 0.95 }}
66
- transition={{ type: 'spring', stiffness: 400, damping: 17 }}
67
- >
68
- <ArrowRight size={20} />
69
- </motion.button>
70
- </div>
71
- </div>
72
-
73
- {/* Right Part: Stacked Images */}
74
- <div className="flex w-full items-center justify-center order-1 md:order-2">
75
- {/* Sized to the card plus the fan, so the stack stays centred and inside the panel. */}
76
- <div className="relative h-[calc(16rem+30px)] w-[calc(12rem+60px)] sm:h-[calc(20rem+30px)] sm:w-[calc(16rem+60px)]">
77
- {testimonials.map((testimonial, index) => {
78
- // Determine the position of the card in the stack
79
- const position = (index - activeIndex + testimonials.length) % testimonials.length;
80
-
81
- // Only render the top 4 cards for performance and visual clarity
82
- if (position > 3) return null;
83
-
84
- return (
85
- <motion.div
86
- key={testimonial.id || index}
87
- className="absolute w-48 h-64 sm:w-64 sm:h-80 bg-slate-800 rounded-xl shadow-2xl overflow-hidden border-2 border-slate-700"
88
- initial={false}
89
- animate={{
90
- scale: 1 - position * 0.08,
91
- rotate: -4 + position * 4, // Add a fanned rotation effect
92
- left: `${position * 20}px`, // Fan out horizontally
93
- top: `${position * 10}px`, // Stagger vertically
94
- zIndex: testimonials.length - position,
95
- opacity: position === 0 ? 1 : 0.5, // Only top image is fully opaque
96
- }}
97
- transition={{
98
- type: 'spring',
99
- stiffness: 260,
100
- damping: 25,
101
- }}
102
- >
103
- <img
104
- src={testimonial.image}
105
- alt={testimonial.author}
106
- className="w-full h-full object-cover"
107
- onError={(e) => { e.target.onerror = null; e.target.src = `https://placehold.co/256x320/0f172a/a3e635?text=${testimonial.author.split(' ')[0]}`; }}
108
- />
109
- </motion.div>
110
- );
111
- })}
112
- </div>
113
- </div>
114
- </div>
115
- );
116
- };
117
-
118
-
119
-
120
- export default Testimonials;
1
+ import React, { useLayoutEffect, useRef, useState } from 'react';
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { ArrowLeft, ArrowRight } from 'lucide-react';
4
+
5
+
6
+ // --- Testimonials Component ---
7
+ const Testimonials = ({ testimonials }) => {
8
+ const [activeIndex, setActiveIndex] = useState(0);
9
+ // The layout follows the card's own width (not the screen), so it also
10
+ // fits sidebars, preview frames and narrow columns.
11
+ const root = useRef(null);
12
+ const [size, setSize] = useState("lg");
13
+
14
+ useLayoutEffect(() => {
15
+ const el = root.current;
16
+ if (!el) return undefined;
17
+ const ro = new ResizeObserver(() => {
18
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
19
+ const w = el.clientWidth / rem;
20
+ setSize(w < 26 ? "sm" : w < 44 ? "md" : "lg");
21
+ });
22
+ ro.observe(el);
23
+ return () => ro.disconnect();
24
+ }, []);
25
+
26
+ const wide = size === "lg";
27
+ const small = size === "sm";
28
+
29
+ const handleNext = () => {
30
+ setActiveIndex((prevIndex) => (prevIndex + 1) % testimonials.length);
31
+ };
32
+
33
+ const handlePrev = () => {
34
+ setActiveIndex((prevIndex) => (prevIndex - 1 + testimonials.length) % testimonials.length);
35
+ };
36
+
37
+ // Animation variants for the text content
38
+ const textVariants = {
39
+ enter: { opacity: 0, y: 20, filter: "blur(5px)" },
40
+ center: { opacity: 1, y: 0, filter: "blur(0px)" },
41
+ exit: { opacity: 0, y: -20, filter: "blur(5px)" },
42
+ };
43
+
44
+ return (
45
+ <div ref={root} className={`w-full max-w-4xl mx-auto bg-slate-950 rounded-2xl border border-slate-800 grid items-center overflow-hidden ${wide ? "grid-cols-2 gap-12 p-12" : small ? "gap-6 p-5" : "gap-8 p-8"}`}>
46
+
47
+ {/* Left Part: Text Content and Navigation */}
48
+ <div className={`flex flex-col justify-center h-full ${wide ? "text-left order-1" : "text-center order-2"}`}>
49
+ {/* Title, subtitle and quote with animation; the min height keeps the buttons from jumping. */}
50
+ <div className={`relative ${small ? "min-h-[15rem]" : "min-h-[12rem]"}`}>
51
+ <AnimatePresence initial={false} mode="wait">
52
+ <motion.div
53
+ key={activeIndex}
54
+ variants={textVariants}
55
+ initial="enter"
56
+ animate="center"
57
+ exit="exit"
58
+ transition={{ duration: 0.4, ease: "easeInOut" }}
59
+ className="w-full"
60
+ >
61
+ <h2 className={`${small ? "text-2xl" : "text-3xl"} font-bold text-white tracking-tight`}>{testimonials[activeIndex].author}</h2>
62
+ <p className="mt-2 text-lime-400 font-medium">{testimonials[activeIndex].title}</p>
63
+ <p className="mt-4 text-slate-400 text-sm leading-relaxed">"{testimonials[activeIndex].text}"</p>
64
+ </motion.div>
65
+ </AnimatePresence>
66
+ </div>
67
+
68
+ {/* Navigation Buttons with Microinteractions */}
69
+ <div className={`flex items-center gap-4 ${wide ? "justify-start mt-8" : "justify-center mt-5"}`}>
70
+ <motion.button
71
+ onClick={handlePrev}
72
+ aria-label="Previous testimonial"
73
+ className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
74
+ whileHover={{ scale: 1.1 }}
75
+ whileTap={{ scale: 0.95 }}
76
+ transition={{ type: 'spring', stiffness: 400, damping: 17 }}
77
+ >
78
+ <ArrowLeft size={20} />
79
+ </motion.button>
80
+ <motion.button
81
+ onClick={handleNext}
82
+ aria-label="Next testimonial"
83
+ className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
84
+ whileHover={{ scale: 1.1 }}
85
+ whileTap={{ scale: 0.95 }}
86
+ transition={{ type: 'spring', stiffness: 400, damping: 17 }}
87
+ >
88
+ <ArrowRight size={20} />
89
+ </motion.button>
90
+ </div>
91
+ </div>
92
+
93
+ {/* Right Part: Stacked Images */}
94
+ <div className={`flex w-full items-center justify-center ${wide ? "order-2" : "order-1"}`}>
95
+ {/* Sized to the card plus the fan, so the stack stays centred and inside the panel. */}
96
+ <div className={`relative ${small ? "h-[calc(14rem+30px)] w-[calc(10.5rem+60px)]" : "h-[calc(20rem+30px)] w-[calc(16rem+60px)]"}`}>
97
+ {testimonials.map((testimonial, index) => {
98
+ // Determine the position of the card in the stack
99
+ const position = (index - activeIndex + testimonials.length) % testimonials.length;
100
+
101
+ // Only render the top 4 cards for performance and visual clarity
102
+ if (position > 3) return null;
103
+
104
+ return (
105
+ <motion.div
106
+ key={testimonial.id || index}
107
+ className={`absolute ${small ? "w-[10.5rem] h-56" : "w-64 h-80"} bg-slate-800 rounded-xl shadow-2xl overflow-hidden border-2 border-slate-700`}
108
+ initial={false}
109
+ animate={{
110
+ scale: 1 - position * 0.08,
111
+ rotate: -4 + position * 4, // Add a fanned rotation effect
112
+ left: `${position * 20}px`, // Fan out horizontally
113
+ top: `${position * 10}px`, // Stagger vertically
114
+ zIndex: testimonials.length - position,
115
+ opacity: position === 0 ? 1 : 0.5, // Only top image is fully opaque
116
+ }}
117
+ transition={{
118
+ type: 'spring',
119
+ stiffness: 260,
120
+ damping: 25,
121
+ }}
122
+ >
123
+ <img
124
+ src={testimonial.image}
125
+ alt={testimonial.author}
126
+ className="w-full h-full object-cover"
127
+ onError={(e) => { e.target.onerror = null; e.target.src = `https://placehold.co/256x320/0f172a/a3e635?text=${testimonial.author.split(' ')[0]}`; }}
128
+ />
129
+ </motion.div>
130
+ );
131
+ })}
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ };
137
+
138
+
139
+
140
+ export default Testimonials;
package/dist/cli/index.js CHANGED
@@ -40,7 +40,6 @@ const componentMap = {
40
40
  "rain-background": "RainBackground",
41
41
  "ripple-background": "RippleBackground",
42
42
  "scroll-marquee": "ScrollMarquee",
43
- "smart-grid-card": "SmartGridCard",
44
43
  "smoke-background": "SmokeBackground",
45
44
  "tooltip": "ToolTip",
46
45
  "tool-tip": "ToolTip",
@@ -153,6 +152,31 @@ const extraDeps = {
153
152
  WebGLCarousel: ["ogl"],
154
153
  CrossedMarquee: ["simple-icons"],
155
154
  };
155
+ // Packages the app already has (or that ship with Node), never installed by us.
156
+ const BUILTIN = new Set(["react", "react-dom", "path", "fs", "url"]);
157
+ // The npm packages a component folder imports, read from its source files.
158
+ async function importedPackages(dir) {
159
+ const found = new Set();
160
+ const walk = async (d) => {
161
+ for (const entry of await fs.readdir(d, { withFileTypes: true })) {
162
+ const full = path.join(d, entry.name);
163
+ if (entry.isDirectory())
164
+ await walk(full);
165
+ else if (/\.(jsx?|tsx?|mjs)$/.test(entry.name)) {
166
+ const code = await fs.readFile(full, "utf8");
167
+ const re = /(?:from\s+|import\s*\(\s*|import\s+)["']([^"'.\/][^"']*)["']/g;
168
+ for (let m = re.exec(code); m; m = re.exec(code)) {
169
+ const spec = m[1];
170
+ const name = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
171
+ if (!BUILTIN.has(name))
172
+ found.add(name);
173
+ }
174
+ }
175
+ }
176
+ };
177
+ await walk(dir);
178
+ return [...found];
179
+ }
156
180
  async function main() {
157
181
  const args = process.argv.slice(2);
158
182
  const command = args[0];
@@ -220,26 +244,6 @@ async function main() {
220
244
  { title: "TypeScript (Coming Soon)", value: "ts", disabled: true },
221
245
  ],
222
246
  });
223
- // Install dependencies
224
- console.log(chalk.cyan("📦 Installing dependencies..."));
225
- const deps = [
226
- "gsap",
227
- "framer-motion",
228
- "three",
229
- "@fortawesome/fontawesome-svg-core",
230
- "@fortawesome/free-solid-svg-icons",
231
- "@fortawesome/react-fontawesome",
232
- "react-icons",
233
- "lucide-react",
234
- ...(extraDeps[matchedName] ?? []),
235
- ];
236
- try {
237
- execSync(`npm install ${deps.join(" ")}`, { stdio: "inherit" });
238
- }
239
- catch (err) {
240
- console.log(chalk.red("❌ Failed to install dependencies."));
241
- return;
242
- }
243
247
  // Prepare destination
244
248
  const apexKitPath = path.join(basePath, "ApexUI-Kit");
245
249
  const dest = path.join(apexKitPath, matchedName);
@@ -250,6 +254,21 @@ async function main() {
250
254
  console.log(chalk.red(`❌ Internal error: Component folder '${matchedName}' not found.`));
251
255
  return;
252
256
  }
257
+ // Install only what this component imports, so the app stays light.
258
+ const deps = [...new Set([...(await importedPackages(source)), ...(extraDeps[matchedName] ?? [])])];
259
+ if (deps.length) {
260
+ console.log(chalk.cyan(`📦 Installing ${deps.join(", ")}...`));
261
+ try {
262
+ execSync(`npm install ${deps.join(" ")}`, { stdio: "inherit" });
263
+ }
264
+ catch (err) {
265
+ console.log(chalk.red("❌ Failed to install dependencies."));
266
+ return;
267
+ }
268
+ }
269
+ else {
270
+ console.log(chalk.cyan("📦 No extra packages needed."));
271
+ }
253
272
  await fs.ensureDir(dest);
254
273
  await fs.copy(source, dest);
255
274
  console.log(chalk.green(`✅ '${matchedName}' added to ${dest}`));
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "apex-ui-kit",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "license": "MIT",
5
- "description": "118 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
5
+ "description": "117 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
6
6
  "homepage": "https://apex-ui.in",
7
7
  "repository": {
8
8
  "type": "git",
@@ -60,5 +60,11 @@
60
60
  "@types/node": "^20.4.0",
61
61
  "copyfiles": "^2.4.1",
62
62
  "typescript": "^5.4.0"
63
+ },
64
+ "sideEffects": [
65
+ "*.css"
66
+ ],
67
+ "engines": {
68
+ "node": ">=18"
63
69
  }
64
70
  }
@@ -1,243 +0,0 @@
1
- import React, { useEffect, useRef, useState } from "react";
2
- import { gsap } from "gsap";
3
- import {
4
- Box,
5
- Settings,
6
- Lock,
7
- Sparkles,
8
- Search,
9
- LaptopMinimal,
10
- } from "lucide-react";
11
-
12
- const CARD_HEIGHT = 264;
13
- const CARD_EXPANDED_HEIGHT = 2 * CARD_HEIGHT + 10; // two rows plus the grid gap
14
-
15
- const SmartGridCard = ({
16
- cards = defaultCards(),
17
- mergeMap = {}, // Example: { 5: 2 }
18
- borderColor = "cyan"
19
- }) => {
20
- // Track screen width for responsive mergeMap logic
21
- const [screenWidth, setScreenWidth] = useState(() => typeof window !== 'undefined' ? window.innerWidth : 1200);
22
- useEffect(() => {
23
- const handleResize = () => setScreenWidth(window.innerWidth);
24
- window.addEventListener('resize', handleResize);
25
- return () => window.removeEventListener('resize', handleResize);
26
- }, []);
27
- const isLargeScreen = screenWidth >= 1080;
28
- const isThreeCol = screenWidth >= 1024;
29
- const isTwoCol = screenWidth >= 768 && screenWidth < 1024;
30
- const cardRefs = useRef({});
31
- const [mousePos, setMousePos] = useState({});
32
-
33
- useEffect(() => {
34
- if (!isLargeScreen) return; // Only run mergeMap effect on large screens
35
- // Defensive: never trigger a reload, only animate
36
- if (Object.keys(mergeMap).length === 0) return; // No merges to process
37
- // Animate cards based on mergeMap
38
- Object.entries(mergeMap).forEach(([sourceId, targetId]) => {
39
- const sourceEl = cardRefs.current[sourceId];
40
- const targetEl = cardRefs.current[targetId];
41
-
42
- if (targetEl) {
43
- gsap.to(targetEl, {
44
- height: CARD_EXPANDED_HEIGHT,
45
- duration: 0.5,
46
- ease: "power2.out",
47
- });
48
- }
49
-
50
- if (sourceEl) {
51
- gsap.to(sourceEl, {
52
- opacity: 0,
53
- scale: 0.9,
54
- duration: 0.3,
55
- onComplete: () => {
56
- if (sourceEl) sourceEl.style.display = "none";
57
- },
58
- });
59
- }
60
- });
61
- // No reload, no navigation, no window.location
62
- return () => { };
63
- }, [mergeMap, isLargeScreen]);
64
-
65
- const handleMouseMove = (e, id) => {
66
- const bounds = e.currentTarget.getBoundingClientRect();
67
- const x = e.clientX - bounds.left;
68
- const y = e.clientY - bounds.top;
69
- setMousePos((prev) => ({
70
- ...prev,
71
- [id]: { x, y },
72
- }));
73
- };
74
-
75
- const getCardStyle = (id) => {
76
- // <768px: 1 column
77
- if (screenWidth < 768) {
78
- return {
79
- display: 'block',
80
- height: CARD_HEIGHT,
81
- gridColumnStart: 'auto',
82
- gridRowStart: 'auto',
83
- };
84
- }
85
- // 768px - 1023px: 2 columns
86
- if (isTwoCol) {
87
- const col = ((id - 1) % 2) + 1;
88
- const row = Math.floor((id - 1) / 2) + 1;
89
- return {
90
- display: 'block',
91
- height: CARD_HEIGHT,
92
- gridColumnStart: col,
93
- gridRowStart: row,
94
- };
95
- }
96
- // 1024px and above: 3 columns (mergeMap only for >=1080px)
97
- if (isThreeCol) {
98
- if (screenWidth < 1080) {
99
- const col = ((id - 1) % 3) + 1;
100
- const row = Math.floor((id - 1) / 3) + 1;
101
- return {
102
- display: 'block',
103
- height: CARD_HEIGHT,
104
- gridColumnStart: col,
105
- gridRowStart: row,
106
- };
107
- }
108
- // >=1080px: 3 columns, merging enabled
109
- const isMerged = mergeMap[id];
110
- const isTarget = Object.values(mergeMap).includes(id);
111
- if (isMerged) return { display: "none" };
112
- const col = ((id - 1) % 3) + 1;
113
- const row = Math.floor((id - 1) / 3) + 1;
114
- return {
115
- gridColumnStart: col,
116
- gridRowStart: row,
117
- height: isTarget ? CARD_EXPANDED_HEIGHT : CARD_HEIGHT,
118
- };
119
- }
120
- };
121
-
122
- return (
123
- <div
124
- className={`grid gap-2 justify-center p-4 w-full ${screenWidth >= 1024 ? 'grid-cols-3' : screenWidth >= 768 ? 'grid-cols-2' : 'grid-cols-1'}`}
125
- style={screenWidth >= 1024 ? {
126
- // minmax(0, …): columns share the container's width instead of overflowing it.
127
- gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
128
- gridTemplateRows: `repeat(2, ${CARD_HEIGHT}px)`
129
- } : screenWidth >= 768 ? {
130
- gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
131
- gridTemplateRows: `repeat(3, ${CARD_HEIGHT}px)`
132
- } : {}}
133
- >
134
- {cards.map((card) => {
135
- const style = getCardStyle(card.id);
136
- if (style.display === "none") return null;
137
-
138
- const pos = mousePos[card.id] || { x: 0, y: 0 };
139
-
140
- return (
141
- <div
142
- key={card.id}
143
- ref={(el) => (cardRefs.current[card.id] = el)}
144
- style={style}
145
- onMouseMove={(e) => handleMouseMove(e, card.id)}
146
- className="group relative overflow-hidden p-[1px] rounded-2xl bg-zinc-900 border border-zinc-700 shadow-2xl transition-all duration-500"
147
- >
148
- {/* Mouse Glow Border */}
149
- <div
150
- className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition duration-300"
151
- style={{
152
- maskImage: `radial-gradient(circle at ${pos.x}px ${pos.y}px, white 40%, transparent 80%)`,
153
- WebkitMaskImage: `radial-gradient(circle at ${pos.x}px ${pos.y}px, white 40%, transparent 80%)`,
154
- background: `radial-gradient(circle at ${pos.x}px ${pos.y}px, ${borderColor}, transparent 80%)`,
155
- borderRadius: "1rem",
156
- }}
157
- />
158
-
159
- {/* Card Content */}
160
- {/* Compact enough that the button always fits inside the fixed card height */}
161
- <div className="relative z-10 flex flex-col items-start h-full w-full p-5 rounded-2xl bg-zinc-900 text-white gap-3">
162
- <div className="flex h-11 w-11 shrink-0 items-center justify-center border border-dashed rounded-lg border-purple-600">{card.icon}</div>
163
- <div className="min-h-0">
164
- <h3 className="text-lg font-bold mb-1.5 break-words leading-tight">{card.title}</h3>
165
- <p className="text-sm leading-relaxed w-full text-white/70 break-words line-clamp-3">{card.description}</p>
166
- </div>
167
- <button
168
- type="button"
169
- className="mt-auto shrink-0 relative overflow-hidden whitespace-nowrap px-4 py-2 rounded-lg border border-purple-400/60 text-white transition hover:text-white bg-black/20 group flex items-center gap-2 text-sm"
170
- >
171
- <span className="relative z-10 flex items-center gap-2">
172
- {card.buttonText}
173
- <span className="arrow-icon transition-transform duration-200 group-hover:translate-x-2 flex items-center">
174
- <svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
175
- <path d="M5 12h14M13 6l6 6-6 6" />
176
- </svg>
177
- </span>
178
- </span>
179
- <span
180
- className="absolute bottom-0 left-0 w-full h-full opacity-0 group-hover:opacity-100 origin-bottom transition-all ease-in-out duration-500 rounded-lg"
181
- style={{
182
- background: "radial-gradient(circle at bottom, purple, transparent 69%)",
183
- mixBlendMode: "screen",
184
- }}
185
- />
186
- </button>
187
- </div>
188
- </div>
189
- );
190
- })}
191
- </div>
192
- );
193
- };
194
-
195
- // Default Cards
196
- function defaultCards() {
197
- return [
198
- {
199
- id: 1,
200
- icon: <LaptopMinimal className="w-6 h-6 text-white" />,
201
- title: "Responsive Layout",
202
- description: "Works across all screen sizes with smooth animation.",
203
- buttonText: "Learn More",
204
- },
205
- {
206
- id: 2,
207
- icon: <Box className="w-6 h-6 text-white" />,
208
- title: "Composable Components",
209
- description: "Each card is modular and easy to customize.",
210
- buttonText: "Explore",
211
- },
212
- {
213
- id: 3,
214
- icon: <Settings className="w-6 h-6 text-white" />,
215
- title: "Easy Config",
216
- description: "Use props like mergeMap to control logic.",
217
- buttonText: "Settings",
218
- },
219
- {
220
- id: 4,
221
- icon: <Lock className="w-6 h-6 text-white" />,
222
- title: "Secure UI",
223
- description: "Perfect for dashboards and secure apps.",
224
- buttonText: "Secure Now",
225
- },
226
- {
227
- id: 5,
228
- icon: <Sparkles className="w-6 h-6 text-white" />,
229
- title: "Edge Effects",
230
- description: "Glowing borders that follow your mouse.",
231
- buttonText: "Try It",
232
- },
233
- {
234
- id: 6,
235
- icon: <Search className="w-6 h-6 text-white" />,
236
- title: "Fast Search",
237
- description: "Lightning-fast component navigation.",
238
- buttonText: "Search",
239
- },
240
- ];
241
- }
242
-
243
- export default SmartGridCard;