apex-ui-kit 2.0.6 → 2.1.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.
Files changed (35) hide show
  1. package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +74 -53
  2. package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +5 -3
  3. package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +14 -2
  4. package/dist/ApexUI-Kit/templates/CardStack/CardStack.jsx +129 -0
  5. package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +3 -3
  6. package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +0 -161
  7. package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +39 -7
  8. package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +116 -98
  9. package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +69 -37
  10. package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +2 -2
  11. package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +14 -13
  12. package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +59 -42
  13. package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +17 -2
  14. package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +18 -3
  15. package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +15 -1
  16. package/dist/ApexUI-Kit/templates/MaskView/MaskView.jsx +345 -0
  17. package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +34 -45
  18. package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +272 -101
  19. package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +8 -2
  20. package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +13 -11
  21. package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +60 -67
  22. package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +69 -55
  23. package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +34 -14
  24. package/dist/ApexUI-Kit/templates/ScrollMarquee/CardMarquee.jsx +0 -0
  25. package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +4 -3
  26. package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +35 -16
  27. package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +11 -8
  28. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +40 -69
  29. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +5 -1
  30. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +25 -7
  31. package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +71 -52
  32. package/dist/cli/index.js +25 -14
  33. package/package.json +2 -2
  34. package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +0 -111
  35. package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +0 -151
@@ -6,7 +6,8 @@ const HoverText = ({
6
6
  fontSize = "2.5rem",
7
7
  className = "",
8
8
  effect, // if not passed, use 'defaultReveal'
9
- effectColor = "#C27AFF"
9
+ effectColor = "#C27AFF",
10
+ textColor = "#fff" // resting colour of the letters
10
11
  }) => {
11
12
  const charsRef = useRef([]);
12
13
  const containerRef = useRef(null);
@@ -32,7 +33,7 @@ const HoverText = ({
32
33
  gsap.to(el, {
33
34
  scale: 1,
34
35
  rotate: 0,
35
- color: "#fff",
36
+ color: textColor,
36
37
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
37
38
  duration: 0.32,
38
39
  ease: "expo.inOut",
@@ -61,7 +62,7 @@ const HoverText = ({
61
62
  gsap.to(el, {
62
63
  scale: 1,
63
64
  rotate: 0,
64
- color: "#fff",
65
+ color: textColor,
65
66
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
66
67
  duration: 0.38,
67
68
  ease: "expo.inOut",
@@ -79,11 +80,11 @@ const HoverText = ({
79
80
  x: 0,
80
81
  y: 0,
81
82
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
82
- color: "#fff",
83
+ color: textColor,
83
84
  });
84
85
  }
85
86
  });
86
- }, [text, effectColor]);
87
+ }, [text, effectColor, textColor]);
87
88
 
88
89
  // Magnetic effect
89
90
  const handleMouseMove = (e) => {
@@ -126,7 +127,7 @@ const HoverText = ({
126
127
  filter: isFocus
127
128
  ? `drop-shadow(0 4px 32px ${effectColor})`
128
129
  : `drop-shadow(0 2px 16px ${effectColor}44)`,
129
- color: isFocus ? effectColor : "#fff",
130
+ color: isFocus ? effectColor : textColor,
130
131
  zIndex: isFocus ? 10 : 2,
131
132
  duration: 0.35,
132
133
  ease: "expo.out",
@@ -156,7 +157,7 @@ const HoverText = ({
156
157
  gsap.to(el, {
157
158
  scaleX: 1,
158
159
  scaleY: 1,
159
- color: "#fff",
160
+ color: textColor,
160
161
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
161
162
  duration: 0.18,
162
163
  ease: "expo.inOut",
@@ -203,7 +204,7 @@ const HoverText = ({
203
204
  y: 0,
204
205
  scale: 1,
205
206
  rotate: 0,
206
- color: "#fff",
207
+ color: textColor,
207
208
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
208
209
  duration: 0.32,
209
210
  ease: "expo.inOut",
@@ -245,7 +246,7 @@ const HoverText = ({
245
246
  gsap.to(el, {
246
247
  scale: 1,
247
248
  rotate: 0,
248
- color: "#fff",
249
+ color: textColor,
249
250
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
250
251
  duration: 0.32,
251
252
  ease: "expo.inOut",
@@ -279,7 +280,7 @@ const HoverText = ({
279
280
  gsap.to(el, {
280
281
  y: 0,
281
282
  scale: 1,
282
- color: "#fff",
283
+ color: textColor,
283
284
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
284
285
  duration: 0.38 + Math.abs(j) * 0.08,
285
286
  ease: "expo.inOut",
@@ -297,7 +298,7 @@ const HoverText = ({
297
298
  y: 0,
298
299
  scale: 1,
299
300
  rotate: 0,
300
- color: "#fff",
301
+ color: textColor,
301
302
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
302
303
  duration: 0.6,
303
304
  ease: "expo.inOut",
@@ -309,7 +310,7 @@ const HoverText = ({
309
310
  if (el) {
310
311
  gsap.to(el, {
311
312
  rotateY: 0,
312
- color: "#fff",
313
+ color: textColor,
313
314
  filter: `drop-shadow(0 0px 0px ${effectColor})`,
314
315
  duration: 0.6,
315
316
  ease: "expo.inOut",
@@ -356,7 +357,7 @@ const HoverText = ({
356
357
  borderRadius: "0.18em",
357
358
  position: "relative",
358
359
  zIndex: 2,
359
- color: "#fff",
360
+ color: textColor,
360
361
  background: "none",
361
362
  transition: "filter 0.3s, color 0.3s, background 0.3s, opacity 0.3s",
362
363
  }}
@@ -1,23 +1,22 @@
1
+ // ...existing code...
1
2
  import React, { useRef, useEffect, useState } from "react";
2
3
  import { gsap } from "gsap";
3
4
  import {
4
5
  faCube,
5
- faChevronRight, // Changed icon for a better look with the new button
6
+ faChevronRight,
6
7
  } from "@fortawesome/free-solid-svg-icons";
7
8
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
8
9
 
9
10
  const HyperCard = ({
10
- // Content props remain unchanged
11
11
  text = "Introducing Hyper Card",
12
12
  LastText = "Effect",
13
- LastTextColor = "#9AE600", // Green color from your original code
13
+ LastTextColor = "#9AE600",
14
14
  SubText = "Fully animated lightning effect card with customizable props, built with Apex UI.",
15
15
  title = "Apex UI Hyper Card",
16
16
  description = "Experience futuristic animations with customizable props like glow, beam, star count, and hover interactions.",
17
17
  buttonText = "Get Started",
18
18
 
19
- // Animation props remain unchanged
20
- starColor = "#9AE600", // Green stars
19
+ starColor = "#9AE600",
21
20
  starCount = 250,
22
21
  glow = true,
23
22
  baseSpeed = 0.5,
@@ -30,18 +29,21 @@ const HyperCard = ({
30
29
  const stars = useRef([]);
31
30
  const speedRef = useRef({ value: baseSpeed });
32
31
 
33
- // 🎇 Particle logic remains exactly the same
32
+ // Particle canvas (kept same logic; responsive CSS classes control visual size)
34
33
  useEffect(() => {
35
34
  const canvas = canvasRef.current;
35
+ if (!canvas) return;
36
36
  const ctx = canvas.getContext("2d");
37
- let animationFrame;
37
+ let animationFrame = 0;
38
38
  let w, h;
39
+ // Only draw while the card is on screen.
40
+ let visible = true;
39
41
 
40
42
  const init = () => {
41
43
  w = canvas.width = canvas.offsetWidth;
42
44
  h = canvas.height = canvas.offsetHeight;
43
45
  stars.current = Array.from({ length: starCount }, () => {
44
- const z = Math.random() * w;
46
+ const z = Math.random() * Math.max(w, 1);
45
47
  return {
46
48
  x: Math.random() * w, y: Math.random() * h,
47
49
  z, baseZ: z, brightness: Math.random() * 0.5 + 0.5,
@@ -50,17 +52,17 @@ const HyperCard = ({
50
52
  };
51
53
 
52
54
  const draw = () => {
53
- // Updated to draw on a transparent background
54
- if (!ctx) return;
55
+ animationFrame = 0;
56
+ if (!ctx || !visible) return;
55
57
  ctx.clearRect(0, 0, w, h);
56
58
  ctx.fillStyle = starColor;
57
59
 
58
60
  stars.current.forEach((star) => {
59
61
  star.z -= speedRef.current.value;
60
- if (star.z <= 1) star.z = star.baseZ;
62
+ if (star.z <= 1) star.z = star.baseZ || Math.max(w, 1);
61
63
  const sx = (star.x - w / 2) * (w / star.z) + w / 2;
62
64
  const sy = (star.y - h / 2) * (h / star.z) + h / 2;
63
- const size = (1 - star.z / w) * 2;
65
+ const size = (1 - star.z / Math.max(w, 1)) * 2;
64
66
  ctx.beginPath();
65
67
  ctx.globalAlpha = star.brightness;
66
68
  if (speedRef.current.value > baseSpeed + 0.3) {
@@ -85,29 +87,46 @@ const HyperCard = ({
85
87
  init();
86
88
  draw();
87
89
  window.addEventListener("resize", init);
88
- return () => { cancelAnimationFrame(animationFrame); window.removeEventListener("resize", init); };
90
+ const visibilityObserver = typeof IntersectionObserver !== "undefined"
91
+ ? new IntersectionObserver(([entry]) => {
92
+ visible = entry.isIntersecting;
93
+ if (visible && !animationFrame) animationFrame = requestAnimationFrame(draw);
94
+ })
95
+ : null;
96
+ visibilityObserver?.observe(canvas);
97
+ return () => {
98
+ cancelAnimationFrame(animationFrame);
99
+ visibilityObserver?.disconnect();
100
+ window.removeEventListener("resize", init);
101
+ };
89
102
  }, [starColor, starCount, baseSpeed]);
90
103
 
91
- // GSAP animations for speed and jiggle remain the same
104
+ // GSAP speed tween (cleaned up)
92
105
  useEffect(() => {
93
- gsap.to(speedRef.current, { value: hovered ? warpSpeed : baseSpeed, duration: 1.5, ease: "power4.inOut" });
106
+ const tween = gsap.to(speedRef.current, { value: hovered ? warpSpeed : baseSpeed, duration: 1.2, ease: "power4.inOut" });
107
+ return () => tween.kill();
94
108
  }, [hovered, warpSpeed, baseSpeed]);
95
109
 
110
+ // Hover jiggle with cleanup
96
111
  useEffect(() => {
97
112
  const el = contentRef.current;
113
+ if (!el) return;
114
+ let jiggle;
98
115
  if (hovered) {
99
- gsap.to(el, { scale: 0.95, duration: 0.5, ease: "power1.out" });
100
- gsap.to(el, { x: "+=1", y: "-=1", duration: 0.05, repeat: -1, yoyo: true, ease: "sine.inOut" });
116
+ gsap.to(el, { scale: 0.95, duration: 0.45, ease: "power1.out" });
117
+ jiggle = gsap.to(el, { x: "+=1", y: "-=1", duration: 0.06, repeat: -1, yoyo: true, ease: "sine.inOut" });
101
118
  } else {
102
119
  gsap.killTweensOf(el);
103
- gsap.to(el, { x: 0, y: 0, scale: 1, duration: 0.4, ease: "power2.out" });
120
+ gsap.to(el, { x: 0, y: 0, scale: 1, duration: 0.35, ease: "power2.out" });
104
121
  }
122
+ return () => jiggle?.kill();
105
123
  }, [hovered]);
106
124
 
107
125
  return (
108
126
  <div
109
- className="w-full max-w-xs mx-auto rounded-2xl overflow-hidden text-white
110
- border border-slate-700 bg-slate-900 shadow-2xl shadow-black/50"
127
+ className="w-full mx-auto rounded-2xl overflow-hidden text-white
128
+ border border-slate-700 bg-slate-900 shadow-2xl shadow-black/50
129
+ max-w-[20rem]"
111
130
  style={{
112
131
  backgroundImage: 'linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px)',
113
132
  backgroundSize: '2rem 2rem'
@@ -115,13 +134,13 @@ const HyperCard = ({
115
134
  >
116
135
  {/* Top animated HyperCard */}
117
136
  <div
118
- className="relative w-full h-50 overflow-hidden group cursor-pointer"
137
+ className="relative w-full overflow-hidden group cursor-pointer
138
+ h-52 md:h-56"
119
139
  onMouseEnter={() => setHovered(true)}
120
140
  onMouseLeave={() => setHovered(false)}
121
141
  >
122
- <canvas ref={canvasRef} className="absolute inset-0 w-full h-full z-0" />
142
+ <canvas ref={canvasRef} className="absolute inset-0 w-full h-full z-0 block" />
123
143
 
124
- {/* Glow effect from your original code */}
125
144
  {glow && (
126
145
  <div
127
146
  className="absolute inset-0 w-full h-full z-10 pointer-events-none"
@@ -133,44 +152,42 @@ const HyperCard = ({
133
152
  />
134
153
  )}
135
154
 
136
- {/* Glowing separator line */}
137
- <div className="absolute bottom-0 w-full h-[2px] bg-purple-500/50 blur-sm"></div>
155
+ <div className="absolute bottom-0 w-full h-[2px] bg-purple-500/50 blur-sm pointer-events-none"></div>
138
156
 
139
- <div ref={contentRef} className="relative z-20 flex flex-col items-center justify-center w-full h-full px-4 py-2">
140
- <h2 className="text-lg sm:text-xl font-normal text-center text-white">
157
+ <div ref={contentRef} className="relative z-20 flex flex-col items-center justify-center w-full h-full px-3 py-2">
158
+ <h2 className="text-sm sm:text-base md:text-lg lg:text-lg font-normal text-center text-white leading-tight">
141
159
  {text}{" "}
142
- {/* Fixed color issue by using inline style */}
143
160
  <span className="font-bold ml-1" style={{ color: LastTextColor }}>
144
161
  {LastText}
145
162
  </span>
146
163
  </h2>
147
- <p className="text-sm text-gray-300 text-center max-w-sm mt-1">{SubText}</p>
164
+ <p className="text-xs sm:text-sm text-gray-300 text-center max-w-xs mt-1 px-2">{SubText}</p>
148
165
  </div>
149
166
  </div>
150
167
 
151
168
  {/* Bottom content section */}
152
- <div className="px-6 pt-5 pb-6 w-full text-left flex flex-col items-start">
153
- <div className="flex items-center gap-3 mb-2">
154
- <FontAwesomeIcon icon={faCube} style={{ color: starColor }} className="text-2xl" />
155
- <h3 className="text-2xl font-extrabold text-white tracking-tight">{title}</h3>
169
+ <div className="px-4 sm:px-5 pt-5 pb-6 w-full text-left flex flex-col items-start">
170
+ <div className="flex items-center gap-2 mb-2">
171
+ <FontAwesomeIcon icon={faCube} style={{ color: starColor }} className="text-lg sm:text-xl" />
172
+ <h3 className="text-xl md:text-2xl font-extrabold text-white tracking-tight leading-tight">{title}</h3>
156
173
  </div>
157
- <p className="text-gray-400 text-base mb-5 max-w-xs leading-relaxed">{description}</p>
174
+ <p className="text-xs sm:text-sm text-gray-400 mb-4 max-w-full leading-relaxed">{description}</p>
158
175
 
159
- {/* --- NEW BUTTON WITH ANIMATION --- */}
160
176
  <button
161
- className="group relative flex items-center gap-2 px-6 py-2.5
177
+ className="group relative flex items-center justify-center gap-2 px-4 py-2
162
178
  border-2 border-[#9AE600] rounded-lg
163
- font-semibold text-white overflow-hidden transition-all duration-300"
179
+ font-semibold text-white overflow-hidden transition-all duration-300
180
+ w-full sm:w-auto"
181
+ aria-label={buttonText}
164
182
  >
165
- {/* Fill-up background effect */}
166
183
  <span
167
- className="absolute inset-0 h-full w-0 bg-[#4c6d0a]
184
+ className="absolute inset-0 h-full w-0 bg-[#4c6d0a]
168
185
  transition-all duration-300 ease-in-out group-hover:w-full"
169
186
  style={{ transformOrigin: 'bottom' }}
187
+ aria-hidden="true"
170
188
  ></span>
171
189
 
172
- {/* Button text and icon */}
173
- <span className="relative z-10 flex items-center gap-2">
190
+ <span className="relative z-10 flex items-center gap-2 text-sm sm:text-base">
174
191
  {buttonText}
175
192
  <FontAwesomeIcon icon={faChevronRight} className="transition-transform duration-300 group-hover:translate-x-1" />
176
193
  </span>
@@ -180,4 +197,4 @@ const HyperCard = ({
180
197
  );
181
198
  };
182
199
 
183
- export default HyperCard;
200
+ export default HyperCard;
@@ -190,7 +190,7 @@ const KineticThreadsBackground = ({
190
190
  targetMouseRef.current.y = 0.5;
191
191
  };
192
192
  if (mouseInteraction) {
193
- window.addEventListener('mousemove', handleMouseMove);
193
+ window.addEventListener('mousemove', handleMouseMove, { passive: true });
194
194
  window.addEventListener('mouseleave', handleMouseLeave);
195
195
  }
196
196
 
@@ -211,12 +211,19 @@ const KineticThreadsBackground = ({
211
211
  uniforms.uResolution.value.set(clientWidth, clientHeight);
212
212
  };
213
213
  window.addEventListener('resize', handleResize);
214
+ // Also follow the container itself (it can change size without a window resize).
215
+ const resizeObserver = new ResizeObserver(() => handleResize());
216
+ resizeObserver.observe(mountNode);
214
217
  handleResize(); // Initial call
215
218
 
216
219
  // Animation loop
217
220
  const clock = new THREE.Clock();
218
- let animationFrameId;
221
+ let animationFrameId = 0;
222
+ // Only render while the background is on screen.
223
+ let visible = true;
219
224
  const animate = () => {
225
+ animationFrameId = 0;
226
+ if (!visible) return;
220
227
  uniforms.uTime.value = clock.getElapsedTime() * speed;
221
228
 
222
229
  // Smoothly update mouse position (lerping)
@@ -228,10 +235,17 @@ const KineticThreadsBackground = ({
228
235
  animationFrameId = requestAnimationFrame(animate);
229
236
  };
230
237
  animate();
238
+ const visibilityObserver = new IntersectionObserver(([entry]) => {
239
+ visible = entry.isIntersecting;
240
+ if (visible && !animationFrameId) animationFrameId = requestAnimationFrame(animate);
241
+ });
242
+ visibilityObserver.observe(mountNode);
231
243
 
232
244
  // Cleanup function
233
245
  return () => {
234
246
  cancelAnimationFrame(animationFrameId);
247
+ visibilityObserver.disconnect();
248
+ resizeObserver.disconnect();
235
249
  window.removeEventListener('resize', handleResize);
236
250
  window.removeEventListener('mousemove', handleMouseMove);
237
251
  window.removeEventListener('mouseleave', handleMouseLeave);
@@ -241,6 +255,7 @@ const KineticThreadsBackground = ({
241
255
  geometry.dispose();
242
256
  material.dispose();
243
257
  renderer.dispose();
258
+ renderer.forceContextLoss();
244
259
  };
245
260
  }, [color, amplitude, distance, speed, mouseInteraction]);
246
261
 
@@ -207,6 +207,9 @@ const LensFlareBackground = ({
207
207
  };
208
208
  updatePlacement();
209
209
  window.addEventListener("resize", updatePlacement);
210
+ // Also follow the container itself (it can change size without a window resize).
211
+ const resizeObserver = new ResizeObserver(() => updatePlacement());
212
+ if (mountRef.current) resizeObserver.observe(mountRef.current);
210
213
 
211
214
  const handleMouseMove = (e) => {
212
215
  if (!mountRef.current) return;
@@ -215,9 +218,13 @@ const LensFlareBackground = ({
215
218
  const y = (e.clientY - rect.top) / rect.height;
216
219
  mouseRef.current = { x, y };
217
220
  };
218
- window.addEventListener("mousemove", handleMouseMove);
221
+ window.addEventListener("mousemove", handleMouseMove, { passive: true });
219
222
 
223
+ // Only render while the background is on screen.
224
+ let visible = true;
220
225
  const animate = (now) => {
226
+ frameId = 0;
227
+ if (!visible) return;
221
228
  uniforms.iTime.value = now * 0.001;
222
229
  const smoothing = 0.92;
223
230
  smoothMouseRef.current.x =
@@ -229,14 +236,22 @@ const LensFlareBackground = ({
229
236
  frameId = requestAnimationFrame(animate);
230
237
  };
231
238
  frameId = requestAnimationFrame(animate);
239
+ const visibilityObserver = new IntersectionObserver(([entry]) => {
240
+ visible = entry.isIntersecting;
241
+ if (visible && !frameId) frameId = requestAnimationFrame(animate);
242
+ });
243
+ if (mountRef.current) visibilityObserver.observe(mountRef.current);
232
244
 
233
245
  return () => {
234
246
  if (frameId) cancelAnimationFrame(frameId);
247
+ visibilityObserver.disconnect();
248
+ resizeObserver.disconnect();
235
249
  window.removeEventListener("resize", updatePlacement);
236
250
  window.removeEventListener("mousemove", handleMouseMove);
237
251
  renderer.dispose();
238
- if (mountRef.current && renderer.domElement.parentNode === mountRef.current) {
239
- mountRef.current.removeChild(renderer.domElement);
252
+ renderer.forceContextLoss();
253
+ if (renderer.domElement.parentNode) {
254
+ renderer.domElement.parentNode.removeChild(renderer.domElement);
240
255
  }
241
256
  };
242
257
  }, [flareColor, intensity, animationSpeed]);
@@ -1,4 +1,4 @@
1
- import React, { useRef, useMemo } from 'react';
1
+ import React, { useRef, useMemo, useEffect, useState } from 'react';
2
2
  import { Canvas, useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  // Helper function to convert hex color to rgba for CSS
@@ -102,8 +102,20 @@ const LuminousParticleOcean = ({
102
102
 
103
103
  const lightColorRgba = useMemo(() => hexToRgba(sphereColor, 0.5), [sphereColor]);
104
104
 
105
+ // Pause the 3D scene while it is off screen.
106
+ const wrapperRef = useRef(null);
107
+ const [inView, setInView] = useState(true);
108
+ useEffect(() => {
109
+ const el = wrapperRef.current;
110
+ if (!el || typeof IntersectionObserver === 'undefined') return undefined;
111
+ const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting));
112
+ observer.observe(el);
113
+ return () => observer.disconnect();
114
+ }, []);
115
+
105
116
  return (
106
117
  <div
118
+ ref={wrapperRef}
107
119
  className="relative w-screen h-[60vh] lg:h-[80vh] overflow-hidden"
108
120
  style={{
109
121
  background: `linear-gradient(to bottom, ${bgColor1}, ${bgColor2})`
@@ -130,6 +142,8 @@ const LuminousParticleOcean = ({
130
142
 
131
143
  <Canvas
132
144
  camera={{ position: [0, 60, 150], fov: 55 }}
145
+ dpr={[1, 1.75]}
146
+ frameloop={inView ? 'always' : 'never'}
133
147
  >
134
148
  {/* Sunlight: Also uses the sphereColor prop */}
135
149
  <pointLight