apex-ui-kit 2.11.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.
package/ReadMe.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/apex-ui-kit?color=a3e635)](https://www.npmjs.com/package/apex-ui-kit)
5
5
  [![license](https://img.shields.io/npm/l/apex-ui-kit?color=a3e635)](./Licence.md)
6
6
 
7
- **118 free, animated UI components for React.** Pick one, add it with a single command, and the source lands in your project so you can read it and change anything.
7
+ **117 free, animated UI components for React.** Pick one, add it with a single command, and the source lands in your project so you can read it and change anything.
8
8
 
9
9
  Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL where a component needs them.
10
10
 
@@ -15,7 +15,7 @@ Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL wh
15
15
 
16
16
  ## Why ApexUI
17
17
 
18
- - **118 components** across backgrounds, cards, text effects, navigation, buttons, interactive UI, AI interfaces, loaders and maps.
18
+ - **117 components** across backgrounds, cards, text effects, navigation, buttons, interactive UI, AI interfaces, loaders and maps.
19
19
  - **You own the code.** The CLI copies plain `.jsx` files into your project, no runtime package to keep in sync.
20
20
  - **Every docs page has** a live preview, a playground to tweak props, a Desktop / Tablet / Mobile preview, the full props table and a ready prompt you can paste into ChatGPT or Claude.
21
21
  - **Smooth on real devices.** Animations stick to transform and opacity, respect reduced motion, and work with mouse, touch and keyboard.
@@ -141,7 +141,6 @@ Every component's props, variants and copy-ready code are on its docs page.
141
141
  | [Pricing Cards](https://apex-ui.in/components/pricing-cards) | `pricing-cards` |
142
142
  | [Profile](https://apex-ui.in/components/profile) | `flip-profile`, `parallax-profile`, `profile-card` |
143
143
  | [Scroll Stack](https://apex-ui.in/components/scroll-stack) | `scroll-stack` |
144
- | [Smart Grid Card](https://apex-ui.in/components/smart-grid-card) | `smart-grid-card` |
145
144
  | [Spotlight Grid](https://apex-ui.in/components/spotlight-grid) | `spotlight-grid` |
146
145
  | [Swipe Cards](https://apex-ui.in/components/swipe-cards) | `swipe-cards` |
147
146
  | [Testimonials](https://apex-ui.in/components/testimonials) | `testimonials` |
@@ -1,4 +1,4 @@
1
- import React, { useId, useRef, useState } from "react";
1
+ import React, { useId, useLayoutEffect, useRef, useState } from "react";
2
2
  import { AnimatePresence, motion } from "framer-motion";
3
3
 
4
4
  /*
@@ -19,7 +19,21 @@ const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accen
19
19
  const [active, setActive] = useState(defaultTab ?? tabs[0]?.id);
20
20
  const [dir, setDir] = useState(1);
21
21
  const refs = useRef([]);
22
+ const root = useRef(null);
22
23
  const dark = theme !== "light";
24
+ // Compact tabs when the box itself is narrow (not the screen), so labels never get cut.
25
+ const [compact, setCompact] = useState(false);
26
+
27
+ useLayoutEffect(() => {
28
+ const el = root.current;
29
+ if (!el) return undefined;
30
+ const ro = new ResizeObserver(() => {
31
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
32
+ setCompact(el.clientWidth / rem < 30);
33
+ });
34
+ ro.observe(el);
35
+ return () => ro.disconnect();
36
+ }, []);
23
37
  const index = tabs.findIndex((t) => t.id === active);
24
38
  const current = tabs[index];
25
39
 
@@ -44,7 +58,7 @@ const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accen
44
58
  }[variant];
45
59
 
46
60
  return (
47
- <div className={`w-full max-w-xl ${dark ? "text-white" : "text-neutral-900"} ${className}`}>
61
+ <div ref={root} className={`w-full max-w-xl ${dark ? "text-white" : "text-neutral-900"} ${className}`}>
48
62
  <div role="tablist" aria-orientation="horizontal" onKeyDown={onKeyDown} className={`relative flex overflow-x-auto [scrollbar-width:none] ${list}`}>
49
63
  {tabs.map((t, i) => {
50
64
  const on = t.id === active;
@@ -58,7 +72,7 @@ const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accen
58
72
  aria-controls={`${id}-panel`}
59
73
  tabIndex={on ? 0 : -1}
60
74
  onClick={() => select(i)}
61
- className={`relative flex-1 whitespace-nowrap px-3 py-2 text-[0.82rem] max-[380px]:px-2 max-[380px]:text-[0.76rem] sm:px-4 sm:text-[0.88rem] font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-current/40 ${variant === "underline" ? "pb-3" : variant === "pill" ? "rounded-full" : "rounded-lg"} ${on ? (variant === "pill" ? "text-neutral-950" : "") : dark ? "text-white/55 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
75
+ className={`relative flex-1 whitespace-nowrap py-2 ${compact ? "px-2 text-[0.76rem]" : "px-4 text-[0.88rem]"} font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-current/40 ${variant === "underline" ? "pb-3" : variant === "pill" ? "rounded-full" : "rounded-lg"} ${on ? (variant === "pill" ? "text-neutral-950" : "") : dark ? "text-white/55 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
62
76
  >
63
77
  {on && (
64
78
  <motion.span
@@ -94,7 +94,7 @@ const FlipClock = ({ mode = "seconds", target, seconds = 3 * 86400 + 4 * 3600 +
94
94
  // The clock is sized in em; on narrow screens the font shrinks with the
95
95
  // container (cqw) so every unit fits instead of being cut off. The min
96
96
  // width keeps it from collapsing inside a shrink-to-fit parent.
97
- <div className="flex w-full justify-center" style={{ containerType: "inline-size", minWidth: `min(${(size * parts.length * 2.55).toFixed(2)}rem, 92vw)` }}>
97
+ <div className="flex w-full justify-center" style={{ containerType: "inline-size", minWidth: `min(${(size * parts.length * 2.55).toFixed(2)}rem, 100%)` }}>
98
98
  <div role="timer" aria-live="off" aria-label={parts.map(([l, v]) => `${v} ${l}`).join(", ")} className={`inline-flex items-start gap-[0.35em] font-mono font-bold ${className}`} style={{ fontSize: `min(${size}rem, ${(100 / (parts.length * 2.55)).toFixed(2)}cqw)` }}>
99
99
  {parts.map(([label, value], i) => (
100
100
  <React.Fragment key={label}>
@@ -123,6 +123,20 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
123
123
  const gooRef = useRef(goo);
124
124
  const glowRef = useRef(theme === "light" ? 0 : 0.22);
125
125
  const dark = theme !== "light";
126
+ // Sized from the space it sits in (not the screen), so it fits narrow columns too.
127
+ const [size, setSize] = useState("lg");
128
+
129
+ useLayoutEffect(() => {
130
+ const el = bar.current?.parentElement?.parentElement;
131
+ if (!el) return undefined;
132
+ const ro = new ResizeObserver(() => {
133
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
134
+ const w = el.clientWidth / rem;
135
+ setSize(w < 22 ? "xs" : w < 34 ? "sm" : "lg");
136
+ });
137
+ ro.observe(el);
138
+ return () => ro.disconnect();
139
+ }, []);
126
140
 
127
141
  useEffect(() => {
128
142
  color.current = hexToRgb(accentColor);
@@ -289,7 +303,7 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
289
303
 
290
304
  return (
291
305
  <nav className={`relative inline-flex max-w-full ${className}`} aria-label="Main">
292
- <div ref={bar} className={`relative flex items-center gap-0 rounded-full p-1 sm:gap-0.5 sm:p-1.5 ${dark ? "bg-neutral-950 ring-1 ring-white/10" : "bg-white shadow-sm ring-1 ring-black/10"}`}>
306
+ <div ref={bar} className={`relative flex items-center gap-0 rounded-full ${size === "lg" ? "gap-0.5 p-1.5" : "p-1"} ${dark ? "bg-neutral-950 ring-1 ring-white/10" : "bg-white shadow-sm ring-1 ring-black/10"}`}>
293
307
  <canvas ref={canvas} aria-hidden="true" className="pointer-events-none absolute" style={{ left: -PAD, top: -PAD, display: fallback ? "none" : "block" }} />
294
308
  {fallback && <span ref={fallbackPill} aria-hidden="true" className="pointer-events-none absolute rounded-full" style={{ background: accentColor }} />}
295
309
  {items.map((item, i) => (
@@ -299,7 +313,7 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
299
313
  href={item.href}
300
314
  onClick={(e) => select(i, e)}
301
315
  aria-current={i === active ? "page" : undefined}
302
- className={`relative z-[1] shrink-0 rounded-full px-2.5 py-2 text-[0.82rem] font-medium max-[380px]:px-2 max-[380px]:text-[0.76rem] sm:py-2.5 sm:text-[0.95rem] outline-none transition-colors duration-300 focus-visible:ring-2 focus-visible:ring-current/30 sm:px-5 ${i === active ? "text-neutral-950" : dark ? "text-white/65 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
316
+ className={`relative z-[1] shrink-0 whitespace-nowrap rounded-full font-medium outline-none transition-colors duration-300 focus-visible:ring-2 focus-visible:ring-current/30 ${{ lg: "px-5 py-2.5 text-[0.95rem]", sm: "px-2.5 py-2 text-[0.82rem]", xs: "px-2 py-2 text-[0.76rem]" }[size]} ${i === active ? "text-neutral-950" : dark ? "text-white/65 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
303
317
  >
304
318
  {item.label}
305
319
  </a>
@@ -24,6 +24,7 @@ const ImageAccordion = ({ items = DEFAULT_ITEMS, defaultActive = 0, autoPlay = 0
24
24
  const list = items.length ? items : DEFAULT_ITEMS;
25
25
  const [active, setActive] = useState(Math.min(defaultActive, list.length - 1));
26
26
  const [vertical, setVertical] = useState(false);
27
+ const [panelW, setPanelW] = useState(416);
27
28
  const root = useRef(null);
28
29
  const hovering = useRef(false);
29
30
 
@@ -34,13 +35,20 @@ const ImageAccordion = ({ items = DEFAULT_ITEMS, defaultActive = 0, autoPlay = 0
34
35
  });
35
36
  };
36
37
 
37
- // Stack vertically when the box is narrow (a container check, not the viewport).
38
+ // Stack vertically when the box is narrow (a container check, not the viewport),
39
+ // and work out how wide the open panel ends up so its text never overflows it.
38
40
  useEffect(() => {
39
41
  const el = root.current;
40
- const ro = new ResizeObserver(([entry]) => setVertical(entry.contentRect.width < 520));
42
+ const ro = new ResizeObserver(([entry]) => {
43
+ const w = entry.contentRect.width;
44
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
45
+ const stacked = w < 520;
46
+ setVertical(stacked);
47
+ setPanelW(stacked ? w : ((w - gap * rem * (list.length - 1)) * openRatio) / (openRatio + list.length - 1));
48
+ });
41
49
  ro.observe(el);
42
50
  return () => ro.disconnect();
43
- }, []);
51
+ }, [gap, openRatio, list.length]);
44
52
 
45
53
  useEffect(() => {
46
54
  if (!autoPlay) return undefined;
@@ -101,8 +109,8 @@ const ImageAccordion = ({ items = DEFAULT_ITEMS, defaultActive = 0, autoPlay = 0
101
109
 
102
110
  {/* Open: title and text rise in once the panel has room. A fixed width keeps them from re-wrapping as it grows. */}
103
111
  <span
104
- className="absolute bottom-0 left-0 block w-[min(26rem,85vw)] p-[clamp(1rem,3cqw,1.75rem)] [backface-visibility:hidden]"
105
- style={{ opacity: on ? 1 : 0, transform: on ? "none" : "translateY(0.75rem)", transition: `opacity ${duration * 0.6}ms ${EASE} ${on ? duration * 0.35 : 0}ms, transform ${duration * 0.8}ms ${EASE} ${on ? duration * 0.3 : 0}ms` }}
112
+ className="absolute bottom-0 left-0 block p-[clamp(1rem,3cqw,1.75rem)] [backface-visibility:hidden]"
113
+ style={{ width: `min(26rem, ${Math.round(panelW)}px)`, opacity: on ? 1 : 0, transform: on ? "none" : "translateY(0.75rem)", transition: `opacity ${duration * 0.6}ms ${EASE} ${on ? duration * 0.35 : 0}ms, transform ${duration * 0.8}ms ${EASE} ${on ? duration * 0.3 : 0}ms` }}
106
114
  >
107
115
  <span className="block font-mono text-[0.7rem] tracking-[0.2em] text-white/60">{String(i + 1).padStart(2, "0")}</span>
108
116
  <span className="mt-1 block truncate text-[1.6rem] font-bold leading-tight">{item.title}</span>
@@ -1,159 +1,176 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { gsap } from "gsap";
3
- const NavMenu = ({
4
- indicatorColor = "#a3e635",
5
- backgroundColor = "rgba(17, 24, 39, 0.5)",
6
- activeColor = "#ffffff",
7
- indicatorAnimation = "elastic",
8
- onNavItemClick
9
- }) => {
10
- const [activeIdx, setActiveIdx] = useState(0);
11
- const navItemsRef = useRef([]);
12
- const indicatorRef = useRef(null);
13
- const navContainerRef = useRef(null);
14
- useEffect(() => {
15
- if (!navContainerRef.current) return;
16
- gsap.fromTo(
17
- navContainerRef.current,
18
- { width: 0, opacity: 0 },
19
- { width: "auto", opacity: 1, duration: 0.8, ease: "power2.out", delay: 0.5 }
20
- );
21
- }, []);
22
-
23
- useEffect(() => {
24
- const indicator = indicatorRef.current;
25
- const el = navItemsRef.current[activeIdx];
26
-
27
- if (!indicator || !el) return;
28
-
29
- const getEase = () => {
30
- switch (indicatorAnimation) {
31
- case "spring": return "bounce.out";
32
- case "power": return "power3.out";
33
- case "elastic":
34
- default: return "elastic.out(1, 0.6)";
35
- }
36
- };
37
-
38
- const moveWithGSAP = () => {
39
- const rect = el.getBoundingClientRect();
40
- const parentRect = el.parentElement.getBoundingClientRect();
41
- const left = rect.left - parentRect.left;
42
- const width = rect.width;
43
-
44
- gsap.to(indicator, {
45
- x: left,
46
- width,
47
- duration: 0.5,
48
- ease: getEase(),
49
- });
50
- };
51
-
52
- const timeoutId = setTimeout(moveWithGSAP, 50);
53
- return () => clearTimeout(timeoutId);
54
-
55
- }, [activeIdx, indicatorAnimation]);
56
-
57
- // Handle resize
58
- useEffect(() => {
59
- const handleResize = () => {
60
- const el = navItemsRef.current[activeIdx];
61
- if (el) {
62
- const rect = el.getBoundingClientRect();
63
- const parentRect = el.parentElement.getBoundingClientRect();
64
- const left = rect.left - parentRect.left;
65
- const width = rect.width;
66
-
67
- gsap.set(indicatorRef.current, {
68
- x: left,
69
- width: width
70
- });
71
- }
72
- };
73
-
74
- window.addEventListener("resize", handleResize);
75
- return () => window.removeEventListener("resize", handleResize);
76
- }, [activeIdx]);
77
-
78
- const navItems = ['Home', 'Docs', 'UI Kit', 'Contact'];
79
-
80
- return (
81
- <nav
82
- ref={navContainerRef}
83
- className="rounded px-3 py-2 shadow-lg border border-white/10 overflow-hidden"
84
- style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: backgroundColor , }}
85
- >
86
- <div
87
- ref={indicatorRef}
88
- className="absolute top-1.5 h-[calc(100%-0.75rem)] rounded-lg z-0 "
89
- style={{ background: indicatorColor, opacity: 0.3 }}
90
- />
91
- <div className="relative flex gap-2 z-10 whitespace-nowrap">
92
- {navItems.map((label, i) => (
93
- <button
94
- key={label}
95
- ref={(el) => (navItemsRef.current[i] = el)}
96
- onClick={() => {
97
- setActiveIdx(i);
98
- if (onNavItemClick) onNavItemClick();
99
- }}
100
- className={`nav-item px-6 py-2 text-base font-semibold rounded-full relative z-10 transition-colors duration-300 ${i === activeIdx ? '' : 'text-gray-400 hover:text-white'}`}
101
- style={{ color: i === activeIdx ? activeColor : undefined }}
102
- >
103
- {label}
104
- </button>
105
- ))}
106
- </div>
107
- </nav>
108
- );
109
- };
110
-
111
-
112
- // Responsive Wrapper Component
113
- const ResponsiveNavMenu = (props) => {
114
- const [isOpen, setIsOpen] = useState(false);
115
- const navItems = ['Home', 'Docs', 'UI Kit', 'Contact'];
116
-
117
- return (
118
- <nav className="flex items-center justify-center p-4 sticky top-0 left-0 w-full z-50 bg-transparent">
119
- <div className="hidden md:flex w-full justify-center">
120
- <NavMenu {...props} />
121
- </div>
122
-
123
- <div className="md:hidden flex w-full justify-end">
124
- {/* Same backdrop as the desktop bar, so the icon stays visible on light pages too. */}
125
- <button
126
- onClick={() => setIsOpen(!isOpen)}
127
- aria-label={isOpen ? "Close menu" : "Open menu"}
128
- className="rounded-full border border-white/10 p-2.5 text-white shadow-lg focus:outline-none"
129
- style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: props.backgroundColor ?? 'rgba(17, 24, 39, 0.7)' }}
130
- >
131
- <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
132
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={isOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16m-7 6h7"} />
133
- </svg>
134
- </button>
135
- </div>
136
-
137
- {isOpen && (
138
- <div className="md:hidden absolute top-20 left-4 right-4 rounded-xl shadow-lg border border-white/10"
139
- style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: 'rgba(17, 24, 39, 0.7)' }}>
140
- <div className="flex flex-col items-center gap-2 p-4">
141
- {navItems.map((label) => (
142
- <button
143
- key={label}
144
- onClick={() => {
145
- console.log(`${label} clicked`);
146
- setIsOpen(false);
147
- }}
148
- className="w-full text-center px-6 py-3 text-lg font-semibold rounded-full text-gray-300 hover:text-white hover:bg-white/10 transition-colors duration-300"
149
- >
150
- {label}
151
- </button>
152
- ))}
153
- </div>
154
- </div>
155
- )}
156
- </nav>
157
- );
158
- };
1
+ import React, { useState, useRef, useEffect, useLayoutEffect } from 'react';
2
+ import { gsap } from "gsap";
3
+ const NavMenu = ({
4
+ indicatorColor = "#a3e635",
5
+ backgroundColor = "rgba(17, 24, 39, 0.5)",
6
+ activeColor = "#ffffff",
7
+ indicatorAnimation = "elastic",
8
+ onNavItemClick
9
+ }) => {
10
+ const [activeIdx, setActiveIdx] = useState(0);
11
+ const navItemsRef = useRef([]);
12
+ const indicatorRef = useRef(null);
13
+ const navContainerRef = useRef(null);
14
+ useEffect(() => {
15
+ if (!navContainerRef.current) return;
16
+ gsap.fromTo(
17
+ navContainerRef.current,
18
+ { width: 0, opacity: 0 },
19
+ { width: "auto", opacity: 1, duration: 0.8, ease: "power2.out", delay: 0.5 }
20
+ );
21
+ }, []);
22
+
23
+ useEffect(() => {
24
+ const indicator = indicatorRef.current;
25
+ const el = navItemsRef.current[activeIdx];
26
+
27
+ if (!indicator || !el) return;
28
+
29
+ const getEase = () => {
30
+ switch (indicatorAnimation) {
31
+ case "spring": return "bounce.out";
32
+ case "power": return "power3.out";
33
+ case "elastic":
34
+ default: return "elastic.out(1, 0.6)";
35
+ }
36
+ };
37
+
38
+ const moveWithGSAP = () => {
39
+ const rect = el.getBoundingClientRect();
40
+ const parentRect = el.parentElement.getBoundingClientRect();
41
+ const left = rect.left - parentRect.left;
42
+ const width = rect.width;
43
+
44
+ gsap.to(indicator, {
45
+ x: left,
46
+ width,
47
+ duration: 0.5,
48
+ ease: getEase(),
49
+ });
50
+ };
51
+
52
+ const timeoutId = setTimeout(moveWithGSAP, 50);
53
+ return () => clearTimeout(timeoutId);
54
+
55
+ }, [activeIdx, indicatorAnimation]);
56
+
57
+ // Handle resize
58
+ useEffect(() => {
59
+ const handleResize = () => {
60
+ const el = navItemsRef.current[activeIdx];
61
+ if (el) {
62
+ const rect = el.getBoundingClientRect();
63
+ const parentRect = el.parentElement.getBoundingClientRect();
64
+ const left = rect.left - parentRect.left;
65
+ const width = rect.width;
66
+
67
+ gsap.set(indicatorRef.current, {
68
+ x: left,
69
+ width: width
70
+ });
71
+ }
72
+ };
73
+
74
+ window.addEventListener("resize", handleResize);
75
+ return () => window.removeEventListener("resize", handleResize);
76
+ }, [activeIdx]);
77
+
78
+ const navItems = ['Home', 'Docs', 'UI Kit', 'Contact'];
79
+
80
+ return (
81
+ <nav
82
+ ref={navContainerRef}
83
+ className="relative rounded px-3 py-2 shadow-lg border border-white/10 overflow-hidden"
84
+ style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: backgroundColor , }}
85
+ >
86
+ <div
87
+ ref={indicatorRef}
88
+ className="absolute top-1.5 h-[calc(100%-0.75rem)] rounded-lg z-0 "
89
+ style={{ background: indicatorColor, opacity: 0.3 }}
90
+ />
91
+ <div className="relative flex gap-2 z-10 whitespace-nowrap">
92
+ {navItems.map((label, i) => (
93
+ <button
94
+ key={label}
95
+ ref={(el) => (navItemsRef.current[i] = el)}
96
+ onClick={() => {
97
+ setActiveIdx(i);
98
+ if (onNavItemClick) onNavItemClick();
99
+ }}
100
+ className={`nav-item px-6 py-2 text-base font-semibold rounded-full relative z-10 transition-colors duration-300 ${i === activeIdx ? '' : 'text-gray-400 hover:text-white'}`}
101
+ style={{ color: i === activeIdx ? activeColor : undefined }}
102
+ >
103
+ {label}
104
+ </button>
105
+ ))}
106
+ </div>
107
+ </nav>
108
+ );
109
+ };
110
+
111
+
112
+ // Responsive Wrapper Component
113
+ const ResponsiveNavMenu = (props) => {
114
+ const [isOpen, setIsOpen] = useState(false);
115
+ const navItems = ['Home', 'Docs', 'UI Kit', 'Contact'];
116
+ // Switch to the menu button when the space it sits in is narrow (not the
117
+ // screen), so it also fits sidebars, preview frames and small columns.
118
+ const wrap = useRef(null);
119
+ const [narrow, setNarrow] = useState(false);
120
+
121
+ useLayoutEffect(() => {
122
+ const el = wrap.current;
123
+ if (!el) return undefined;
124
+ const ro = new ResizeObserver(() => {
125
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
126
+ setNarrow(el.clientWidth / rem < 34);
127
+ });
128
+ ro.observe(el);
129
+ return () => ro.disconnect();
130
+ }, []);
131
+
132
+ return (
133
+ <nav ref={wrap} className="flex items-center justify-center p-4 sticky top-0 left-0 w-full z-50 bg-transparent">
134
+ {!narrow ? (
135
+ <div className="flex w-full justify-center">
136
+ <NavMenu {...props} />
137
+ </div>
138
+ ) : (
139
+ <div className="flex w-full justify-end">
140
+ {/* Same backdrop as the desktop bar, so the icon stays visible on light pages too. */}
141
+ <button
142
+ onClick={() => setIsOpen(!isOpen)}
143
+ aria-label={isOpen ? "Close menu" : "Open menu"}
144
+ className="rounded-full border border-white/10 p-2.5 text-white shadow-lg focus:outline-none"
145
+ style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: props.backgroundColor ?? 'rgba(17, 24, 39, 0.7)' }}
146
+ >
147
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
148
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={isOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16m-7 6h7"} />
149
+ </svg>
150
+ </button>
151
+ </div>
152
+ )}
153
+
154
+ {narrow && isOpen && (
155
+ <div className="absolute top-20 left-4 right-4 rounded-xl shadow-lg border border-white/10"
156
+ style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: 'rgba(17, 24, 39, 0.7)' }}>
157
+ <div className="flex flex-col items-center gap-2 p-4">
158
+ {navItems.map((label) => (
159
+ <button
160
+ key={label}
161
+ onClick={() => {
162
+ setIsOpen(false);
163
+ props.onNavItemClick?.();
164
+ }}
165
+ className="w-full text-center px-6 py-3 text-lg font-semibold rounded-full text-gray-300 hover:text-white hover:bg-white/10 transition-colors duration-300"
166
+ >
167
+ {label}
168
+ </button>
169
+ ))}
170
+ </div>
171
+ </div>
172
+ )}
173
+ </nav>
174
+ );
175
+ };
159
176
  export default ResponsiveNavMenu;
@@ -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",
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "apex-ui-kit",
3
- "version": "2.11.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",
@@ -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;