# Lightswind UI — Full LLM Reference > Website: https://lightswind.com > Website: https://lightswind.com > This reference documents Free components, Pro components, and Pro blocks. > Source code is provided for Free components only. Premium components/blocks contain metadata and usage specs. --- INSTALLATION --- 1. `npm install framer-motion lucide-react clsx tailwind-merge class-variance-authority` 2. Setup `lib/utils.ts` with `cn` helper. --- PREMIUM ACCESS & LICENSING --- To access the source code for any Pro component or Block marked with (Source code excluded): 1. Visit the pricing page: https://lightswind.com/pricing 2. Purchase a lifetime or developer license. 3. Log in to your account at https://lightswind.com/profile. 4. Copy the component/block code directly from the interactive preview dashboards. --- FREE COMPONENTS (WITH SOURCE CODE) (213) --- ## CATEGORY (FREE): Utilities (2) ### COMPONENT: cool-theme-toggle Category: Utilities Description: A professional and beautiful theme toggle with smooth spring animations, sun/moon icons, and day/night scenery background. URL: https://lightswind.com/components/cool-theme-toggle Import: import { CoolThemeToggle } from "@/components/lightswind/cool-theme-toggle" Registry URL: https://lightswind.com/r/cool-theme-toggle.json Install Command: npx lightswind@latest add cool-theme-toggle Usage: ```tsx import { CoolThemeToggle } from "@/components/lightswind/cool-theme-toggle"; export function CoolThemeToggleDemo() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useState, useEffect } from "react"; import { motion } from "framer-motion"; import { Sun, Moon, Cloud, Star } from "lucide-react"; import { cn } from "@/components/lib/utils"; interface CoolThemeToggleProps { className?: string; size?: "sm" | "md" | "lg"; } export function CoolThemeToggle({ className, size = "md" }: CoolThemeToggleProps) { const [theme, setTheme] = useState<"light" | "dark">("light"); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); const isDark = document.documentElement.classList.contains("dark"); setTheme(isDark ? "dark" : "light"); const observer = new MutationObserver(() => { setTheme(document.documentElement.classList.contains("dark") ? "dark" : "light"); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] }); return () => observer.disconnect(); }, []); const toggleTheme = () => { const newTheme = theme === "light" ? "dark" : "light"; setTheme(newTheme); if (newTheme === "dark") { document.documentElement.classList.add("dark"); localStorage.setItem("theme", "dark"); } else { document.documentElement.classList.remove("dark"); localStorage.setItem("theme", "light"); } }; if (!mounted) return
; const sizes = { sm: { button: "w-12 h-6", thumb: "w-4 h-4", icon: "w-2.5 h-2.5", padding: "p-1", translateX: "translateX(24px)", cloudSize: "w-3 h-3" }, md: { button: "w-16 h-8", thumb: "w-6 h-6", icon: "w-4 h-4", padding: "p-1", translateX: "translateX(32px)", cloudSize: "w-5 h-5" }, lg: { button: "w-20 h-10", thumb: "w-8 h-8", icon: "w-5 h-5", padding: "p-1", translateX: "translateX(40px)", cloudSize: "w-6 h-6" } }; const currentSize = sizes[size]; return ( ); } ``` -------------------------------------------------- ### COMPONENT: toggle-theme Category: Utilities Description: An accessible button component that toggles between light and dark themes, utilizing the modern View Transition API for smooth, customizable full-page animations. URL: https://lightswind.com/components/toggle-theme Import: import { ToggleTheme } from "@/components/lightswind/toggle-theme"; Registry URL: https://lightswind.com/r/toggle-theme.json Install Command: npx lightswind@latest add toggle-theme Usage: ```tsx // 1. Simple usage with default 'circle-spread' animation // 2. Controlled duration and custom animation ``` Source Code: ```tsx "use client" import { useCallback, useEffect, useRef, useState } from "react" import { Moon, Sun } from "lucide-react" import { flushSync } from "react-dom" import { cn } from "@/components/lib/utils" // 1. Define the possible animation types (UPDATED to include all demo types) // NOTE: Type is renamed from 'AnimationType' to 'ThemeAnimationType' // to avoid conflicts if used with the demo file in the same scope, // though the original 'AnimationType' is kept for minimal change. type AnimationType = | "none" | "circle-spread" | "round-morph" | "swipe-left" | "swipe-up" | "diag-down-right" | "fade-in-out" | "shrink-grow" | "flip-x-in" | "split-vertical" | "swipe-right" | "swipe-down" | "wave-ripple" // 2. Interface is renamed interface ToggleThemeProps extends React.ComponentPropsWithoutRef<"button"> { duration?: number animationType?: AnimationType } // 3. Component and export are renamed export const ToggleTheme = ({ className, duration = 400, animationType = "circle-spread", ...props }: ToggleThemeProps) => { const [isDark, setIsDark] = useState(false) const buttonRef = useRef(null) useEffect(() => { const updateTheme = () => { setIsDark(document.documentElement.classList.contains("dark")) } updateTheme() const observer = new MutationObserver(updateTheme) observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"], }) return () => observer.disconnect() }, []) useEffect(() => { if (animationType === 'flip-x-in') return let styleElement = document.getElementById("toggle-theme-vt-override") as HTMLStyleElement if (!styleElement) { styleElement = document.createElement("style") styleElement.id = "toggle-theme-vt-override" styleElement.textContent = ` ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; } ` document.head.appendChild(styleElement) } }, [animationType]) const toggleTheme = useCallback(async () => { if (!buttonRef.current) return // Wait for the DOM update to complete within the View Transition await document.startViewTransition(() => { flushSync(() => { const newTheme = !isDark setIsDark(newTheme) document.documentElement.classList.toggle("dark") localStorage.setItem("theme", newTheme ? "dark" : "light") }) }).ready // Calculate coordinates and dimensions for spatial animations const { top, left, width, height } = buttonRef.current.getBoundingClientRect() const x = left + width / 2 const y = top + height / 2 const maxRadius = Math.hypot( Math.max(left, window.innerWidth - left), Math.max(top, window.innerHeight - top) ) const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight // 4. Implement a switch to handle all animation types switch (animationType) { // --- Existing/Refined Types --- case "circle-spread": document.documentElement.animate( { clipPath: [ `circle(0px at ${x}px ${y}px)`, `circle(${maxRadius}px at ${x}px ${y}px)`, ], }, { duration, easing: "ease-in-out", pseudoElement: "::view-transition-new(root)", } ) break case "round-morph": document.documentElement.animate( [ { opacity: 0, transform: "scale(0.8) rotate(5deg)" }, { opacity: 1, transform: "scale(1) rotate(0deg)" }, ], { duration: duration * 1.2, easing: "cubic-bezier(0.68, -0.55, 0.265, 1.55)", pseudoElement: "::view-transition-new(root)", } ) break case "swipe-left": document.documentElement.animate( { clipPath: [ `inset(0 0 0 ${viewportWidth}px)`, `inset(0 0 0 0)`, ], }, { duration, easing: "cubic-bezier(0.2, 0, 0, 1)", pseudoElement: "::view-transition-new(root)", } ) break case "swipe-up": document.documentElement.animate( { clipPath: [ `inset(${viewportHeight}px 0 0 0)`, `inset(0 0 0 0)`, ], }, { duration, easing: "cubic-bezier(0.2, 0, 0, 1)", pseudoElement: "::view-transition-new(root)", } ) break // --- New Advanced Types --- case "diag-down-right": document.documentElement.animate( { clipPath: [ `polygon(0 0, 0 0, 0 0, 0 0)`, `polygon(0 0, 100% 0, 100% 100%, 0 100%)`, ], }, { duration: duration * 1.5, easing: "cubic-bezier(0.4, 0, 0.2, 1)", pseudoElement: "::view-transition-new(root)", } ) break case "fade-in-out": document.documentElement.animate( { opacity: [0, 1], }, { duration: duration * 0.5, easing: "ease-in-out", pseudoElement: "::view-transition-new(root)", } ) break case "shrink-grow": document.documentElement.animate( [ { transform: "scale(0.9)", opacity: 0 }, { transform: "scale(1)", opacity: 1 }, ], { duration: duration * 1.2, easing: "cubic-bezier(0.19, 1, 0.22, 1)", pseudoElement: "::view-transition-new(root)", } ) document.documentElement.animate( [ { transform: "scale(1)", opacity: 1 }, { transform: "scale(1.05)", opacity: 0 }, ], { duration: duration * 1.2, easing: "cubic-bezier(0.19, 1, 0.22, 1)", pseudoElement: "::view-transition-old(root)", } ) break case "flip-x-in": const styleElement = document.createElement('style'); styleElement.textContent = ` ::view-transition-group(root) { perspective: 1000px; } ::view-transition-old(root) { transform-origin: center; animation: flip-out 400ms forwards; } ::view-transition-new(root) { transform-origin: center; animation: flip-in 400ms forwards; } @keyframes flip-out { from { transform: rotateY(0deg); opacity: 1; } to { transform: rotateY(-90deg); opacity: 0; } } @keyframes flip-in { from { transform: rotateY(90deg); opacity: 0; } to { transform: rotateY(0deg); opacity: 1; } } `; document.head.appendChild(styleElement); break case "split-vertical": document.documentElement.animate( [{ opacity: 0 }, { opacity: 1 }], { duration: duration * 0.75, easing: "ease-in", pseudoElement: "::view-transition-new(root)", } ) document.documentElement.animate( [ { clipPath: 'inset(0 0 0 0)', transform: 'none' }, { clipPath: 'inset(0 40% 0 40%)', transform: 'scale(1.2)' }, { clipPath: 'inset(0 50% 0 50%)', transform: 'scale(1)' }, ], { duration: duration * 1.5, easing: "cubic-bezier(0.68, -0.55, 0.265, 1.55)", pseudoElement: "::view-transition-old(root)", } ) break // --- IMPLEMENTATION FOR MISSING TYPES --- case "swipe-right": document.documentElement.animate( { clipPath: [ `inset(0 ${viewportWidth}px 0 0)`, `inset(0 0 0 0)`, ], }, { duration, easing: "cubic-bezier(0.2, 0, 0, 1)", pseudoElement: "::view-transition-new(root)", } ) break case "swipe-down": document.documentElement.animate( { clipPath: [ `inset(0 0 ${viewportHeight}px 0)`, `inset(0 0 0 0)`, ], }, { duration, easing: "cubic-bezier(0.2, 0, 0, 1)", pseudoElement: "::view-transition-new(root)", } ) break case "wave-ripple": document.documentElement.animate( { clipPath: [ `circle(0% at 50% 50%)`, `circle(${maxRadius}px at 50% 50%)`, ], }, { duration: duration * 1.5, easing: "cubic-bezier(0.68, -0.55, 0.265, 1.55)", pseudoElement: "::view-transition-new(root)", } ) break case "none": default: // No custom animation runs break } }, [isDark, duration, animationType]) return ( ) } ``` -------------------------------------------------- ## CATEGORY (FREE): 3D Elements (25) ### COMPONENT: 3d-image-ring Category: 3D Elements Description: A stunning 3D image carousel arranged in a circular ring that users can drag to rotate. Features smooth GSAP animations, parallax effects, and responsive touch controls. URL: https://lightswind.com/components/3d-image-ring Import: import { ThreeDImageRing } from "@/components/lightswind/draggable-3d-image-ring"; Registry URL: https://lightswind.com/r/3d-image-ring.json Install Command: npx lightswind@latest add 3d-image-ring Usage: ```tsx const imageUrls = [ "https://images.pexels.com/photos/1704120/pexels-photo-1704120.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/1103970/pexels-photo-1103970.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/2387873/pexels-photo-2387873.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/912110/pexels-photo-912110.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/325185/pexels-photo-325185.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/114979/pexels-photo-114979.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/1108099/pexels-photo-1108099.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/145939/pexels-photo-145939.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/698808/pexels-photo-698808.jpeg?auto=compress&cs=tinysrgb&w=1200", "https://images.pexels.com/photos/2449540/pexels-photo-2449540.jpeg?auto=compress&cs=tinysrgb&w=1200", ]; ``` Source Code: ```tsx "use client"; import React, { useEffect, useRef, useState, useMemo } from "react"; import { motion, AnimatePresence, useMotionValue, easeOut } from "framer-motion"; import { cn } from "@/components/lib/utils"; // Assuming you have this utility for class names import { animate } from "framer-motion"; export interface ThreeDImageRingProps { /** Array of image URLs to display in the ring */ images: string[]; /** Container width in pixels (will be scaled) */ width?: number; /** 3D perspective value */ perspective?: number; /** Distance of images from center (z-depth) */ imageDistance?: number; /** Initial rotation of the ring */ initialRotation?: number; /** Animation duration for entrance */ animationDuration?: number; /** Stagger delay between images */ staggerDelay?: number; /** Hover opacity for non-hovered images */ hoverOpacity?: number; /** Custom container className */ containerClassName?: string; /** Custom ring className */ ringClassName?: string; /** Custom image className */ imageClassName?: string; /** Background color of the stage */ backgroundColor?: string; /** Enable/disable drag functionality */ draggable?: boolean; /** Animation ease for entrance */ ease?: string; /** Breakpoint for mobile responsiveness (e.g., 768 for iPad mini) */ mobileBreakpoint?: number; /** Scale factor for mobile (e.g., 0.7 for 70% size) */ mobileScaleFactor?: number; /** Power for the drag end inertia animation (higher means faster stop) */ inertiaPower?: number; /** Time constant for the drag end inertia animation (duration of deceleration in ms) */ inertiaTimeConstant?: number; /** Multiplier for initial velocity when drag ends (influences initial "spin") */ inertiaVelocityMultiplier?: number; } export function ThreeDImageRing({ images = [ "https://images.unsplash.com/photo-1510812431401-41d2bd2722f3?q=80&w=2940&auto=format&fit=crop", "https://images.unsplash.com/photo-1469474968028-56623f02e42e?q=80&w=2938&auto=format&fit=crop", "https://images.unsplash.com/photo-1506744626753-1fa7604d459a?q=80&w=2940&auto=format&fit=crop", "https://images.unsplash.com/photo-1470071131384-001b85755536?q=80&w=2940&auto=format&fit=crop", "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?q=80&w=2940&auto=format&fit=crop", ], width = 300, perspective = 2000, imageDistance = 500, initialRotation = 180, animationDuration = 1.5, staggerDelay = 0.1, hoverOpacity = 0.5, containerClassName, ringClassName, imageClassName, backgroundColor, draggable = true, ease = "easeOut", mobileBreakpoint = 768, mobileScaleFactor = 0.8, inertiaPower = 0.8, // Default power for inertia inertiaTimeConstant = 300, // Default time constant for inertia inertiaVelocityMultiplier = 20, // Default multiplier for initial spin }: ThreeDImageRingProps) { const containerRef = useRef(null); const ringRef = useRef(null); const rotationY = useMotionValue(initialRotation); const startX = useRef(0); const currentRotationY = useRef(initialRotation); const isDragging = useRef(false); const velocity = useRef(0); // To track drag velocity const [currentScale, setCurrentScale] = useState(1); const [showImages, setShowImages] = useState(false); const angle = useMemo(() => 360 / images.length, [images.length]); const getBgPos = (imageIndex: number, currentRot: number, scale: number) => { const scaledImageDistance = imageDistance * scale; const effectiveRotation = currentRot - 180 - imageIndex * angle; const parallaxOffset = ((effectiveRotation % 360 + 360) % 360) / 360; return `${-(parallaxOffset * (scaledImageDistance / 1.5))}px 0px`; }; useEffect(() => { const unsubscribe = rotationY.on("change", (latestRotation) => { if (ringRef.current) { Array.from(ringRef.current.children).forEach((imgElement, i) => { (imgElement as HTMLElement).style.backgroundPosition = getBgPos( i, latestRotation, currentScale ); }); } currentRotationY.current = latestRotation; }); return () => unsubscribe(); }, [rotationY, images.length, imageDistance, currentScale, angle]); useEffect(() => { const handleResize = () => { const viewportWidth = window.innerWidth; const newScale = viewportWidth <= mobileBreakpoint ? mobileScaleFactor : 1; setCurrentScale(newScale); }; window.addEventListener("resize", handleResize); handleResize(); return () => window.removeEventListener("resize", handleResize); }, [mobileBreakpoint, mobileScaleFactor]); useEffect(() => { setShowImages(true); }, []); const handleDragStart = (event: React.MouseEvent | React.TouchEvent) => { if (!draggable) return; isDragging.current = true; const clientX = "touches" in event ? event.touches[0].clientX : event.clientX; startX.current = clientX; // Stop any ongoing animation instantly when drag starts rotationY.stop(); velocity.current = 0; // Reset velocity if (ringRef.current) { (ringRef.current as HTMLElement).style.cursor = "grabbing"; } // Attach global move and end listeners to document when dragging starts document.addEventListener("mousemove", handleDrag); document.addEventListener("mouseup", handleDragEnd); document.addEventListener("touchmove", handleDrag); document.addEventListener("touchend", handleDragEnd); }; const handleDrag = (event: MouseEvent | TouchEvent) => { // Only proceed if dragging is active if (!draggable || !isDragging.current) return; const clientX = "touches" in event ? (event as TouchEvent).touches[0].clientX : (event as MouseEvent).clientX; const deltaX = clientX - startX.current; // Update velocity based on deltaX velocity.current = -deltaX * 0.5; // Factor of 0.5 to control sensitivity rotationY.set(currentRotationY.current + velocity.current); startX.current = clientX; }; const handleDragEnd = () => { isDragging.current = false; if (ringRef.current) { ringRef.current.style.cursor = "grab"; currentRotationY.current = rotationY.get(); } document.removeEventListener("mousemove", handleDrag); document.removeEventListener("mouseup", handleDragEnd); document.removeEventListener("touchmove", handleDrag); document.removeEventListener("touchend", handleDragEnd); const initial = rotationY.get(); const velocityBoost = velocity.current * inertiaVelocityMultiplier; const target = initial + velocityBoost; // Animate with inertia manually using `animate()` animate(initial, target, { type: "inertia", velocity: velocityBoost, power: inertiaPower, timeConstant: inertiaTimeConstant, restDelta: 0.5, modifyTarget: (target) => Math.round(target / angle) * angle, onUpdate: (latest) => { rotationY.set(latest); }, }); velocity.current = 0; }; // Corrected imageVariants: no function for 'visible' state const imageVariants = { hidden: { y: 200, opacity: 0 }, visible: { y: 0, opacity: 1, // Transition properties will be defined directly on the motion.div using `custom` prop }, }; return (
{showImages && images.map((imageUrl, index) => ( { // Prevent hover effects while dragging if (isDragging.current) return; if (ringRef.current) { Array.from(ringRef.current.children).forEach((imgEl, i) => { if (i !== index) { (imgEl as HTMLElement).style.opacity = `${hoverOpacity}`; } }); } }} onHoverEnd={() => { // Prevent hover effects while dragging if (isDragging.current) return; if (ringRef.current) { Array.from(ringRef.current.children).forEach((imgEl) => { (imgEl as HTMLElement).style.opacity = `1`; }); } }} /> ))}
); } export default ThreeDImageRing; ``` -------------------------------------------------- ### COMPONENT: ascii-wave Category: 3D Elements Description: A retro-style, text-based animation using ASCII characters to simulate fluids, fire, or data streams on HTML Canvas. URL: https://lightswind.com/components/ascii-wave Import: import AsciiWave from "@/components/lightswind/ascii-wave" Registry URL: https://lightswind.com/r/ascii-wave.json Install Command: npx lightswind@latest add ascii-wave Usage: ```tsx import AsciiWave from "@/components/lightswind/ascii-wave"; export function AsciiWaveDemo() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useRef, useEffect } from "react"; import { useTheme } from "next-themes"; interface AsciiWaveProps { className?: string; color?: string; // Hex or generic color speed?: number; } const AsciiWave: React.FC = ({ className, color = "#FF4500", // Default Firecrawl Orange speed = 1 }) => { const canvasRef = useRef(null); const containerRef = useRef(null); const { theme } = useTheme() || {}; useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let animationId: number; let time = 0; const resize = () => { if (!container || !canvas) return; const dpr = window.devicePixelRatio || 1; canvas.width = container.clientWidth * dpr; canvas.height = container.clientHeight * dpr; ctx.scale(dpr, dpr); }; const observer = new ResizeObserver(resize); observer.observe(container); resize(); // ASCII Characters sorted by density (light to dark) const chars = " .:+x*#".split(""); const fontSize = 12; const columnWidth = 10; const draw = () => { // Clean clear for crisp pixels. const width = container.clientWidth; const height = container.clientHeight; ctx.clearRect(0, 0, width, height); ctx.font = `${fontSize}px monospace`; ctx.fillStyle = color; const columns = Math.ceil(width / columnWidth); const rows = Math.ceil(height / fontSize); // "Fire" Logic: Pure Vertical Ascent for (let x = 0; x < columns; x++) { // FIXED GEOMETRY (No traveling waves!) // Amplitude modulation only (Standing Wave) // x * 0.1 gives the mountain shape. // We do NOT add 'time' to 'x' here. const shapeBase = Math.sin(x * 0.1) * 0.6 + Math.cos(x * 0.25) * 0.4; // Breath: Global pulsing to make it feel alive const breath = Math.sin(time * 0.002 * speed) * 0.1; // Flicker: High frequency jitter that does NOT travel const flicker = Math.sin(time * 0.008 * speed + x * 100) * 0.05; // Height calculation const noise = shapeBase + breath + flicker; const columnHeightNormal = Math.max(0.15, (noise + 1) / 2 * 0.6 + 0.15); const activeRows = Math.floor(columnHeightNormal * rows); for (let y = rows - 1; y > rows - activeRows; y--) { // PURE VERTICAL FLOW // No horizontal phase mixing (x * k) in the time component const flowShift = time * 0.005 * speed; // Independent column noise // y moves up over time (y - flowShift). // x acts only as a static seed/offset for variety between columns. const charNoise = Math.sin((y * 0.2) - flowShift + x * 10); // Top fade const distFromTop = (y - (rows - activeRows)); const fade = Math.min(1, distFromTop / 6); // Char selection const normalizedNoise = (charNoise + 1) / 2; const charIndex = Math.floor(normalizedNoise * chars.length); const char = chars[Math.min(charIndex, chars.length - 1)]; const posX = x * columnWidth; const posY = y * fontSize; // Glitch dropouts (holes in the flame) if (Math.random() > 0.95) continue; ctx.globalAlpha = fade; ctx.fillText(char, posX, posY); } } ctx.globalAlpha = 1.0; time += 16; animationId = requestAnimationFrame(draw); }; animationId = requestAnimationFrame(draw); return () => { observer.disconnect(); cancelAnimationFrame(animationId); }; }, [color, speed, theme]); return (
); }; export default AsciiWave; ``` -------------------------------------------------- ### COMPONENT: 3d-image-carousel Category: 3D Elements Description: A responsive, 3D-styled image slider with a cascade effect that smoothly transitions between slides. Supports drag, touch, and autoplay interactions, while remaining fully Tailwind-compatible for design customization. Only minimal embedded CSS is used to maintain the 3D stacking and positioning logic. URL: https://lightswind.com/components/3d-image-carousel Import: import ThreeDImageCarousel from "@/components/lightswind/ThreeDImageCarousel"; Registry URL: https://lightswind.com/r/3d-image-carousel.json Install Command: npx lightswind@latest add 3d-image-carousel Usage: ```tsx // 1. Default Usage (5 visible items, no autoplay) const slides = [ { id: 1, src: "/images/slide1.jpg", href: "/product/1" }, { id: 2, src: "/images/slide2.jpg", href: "/product/2" }, { id: 3, src: "/images/slide3.jpg", href: "/product/3" }, { id: 4, src: "/images/slide4.jpg", href: "/product/4" }, { id: 5, src: "/images/slide5.jpg", href: "/product/5" }, ]; // 2. Autoplay enabled with 4-second delay // 3. Show only 3 visible items (center-focused layout) // 4. Disable pause on hover (continuous autoplay) // 5. Custom styled container using Tailwind utilities ``` Source Code: ```tsx 'use client' import { ArrowLeftCircle, ArrowRightCircle } from 'lucide-react'; import React, { useState, useEffect, useRef, useCallback } from 'react'; // --- Type Definitions --- interface Slide { id: number; src: string; href: string; } interface ThreeDImageCarouselProps { /** The array of image data for the slider. */ slides: Slide[]; /** Number of visible items in the slider (3 or 5). Default is 5. */ itemCount?: 3 | 5; /** Enables/Disables automatic sliding. Default is false. */ autoplay?: boolean; /** Delay in seconds for autoplay. Default is 3. */ delay?: number; /** Pauses autoplay when the mouse hovers over the slider. Default is true. */ pauseOnHover?: boolean; /** Tailwind class for the main container (e.g., margins, padding). */ className?: string; } // --- MINIMIZED CSS Styles (Only core 3D positioning and responsiveness remain) --- const EMBEDDED_CSS = ` /* --- Cascade Slider Styles --- */ .cascade-slider_container { position: relative; max-width: 1000px; margin: 0 auto; z-index: 20; user-select: none; -webkit-user-select: none; touch-action: pan-y; } .cascade-slider_slides { position: relative; height: 100%; } .cascade-slider_item { position: absolute; top: 50%; left: 50%; transform: translateY(-50%) translateX(-50%) scale(0.3); transition: all 1s ease; opacity: 0; z-index: 1; cursor: grab; } .cascade-slider_item.now { cursor: default; } .cascade-slider_item:active { cursor: grabbing; } /* Slide Positioning Classes (Core 3D Logic - MUST REMAIN IN CSS) */ .cascade-slider_item.next { left: 50%; transform: translateY(-50%) translateX(-120%) scale(0.6); opacity: 1; z-index: 4; } .cascade-slider_item.prev { left: 50%; transform: translateY(-50%) translateX(20%) scale(0.6); opacity: 1; z-index: 4; } .cascade-slider_item.now { top: 50%; left: 50%; transform: translateY(-50%) translateX(-50%) scale(1); opacity: 1; z-index: 5; } /* Arrows - Structural CSS remains for positioning/size */ .cascade-slider_arrow { display: flex; align-items: center; justify-content: center; position: absolute; top: 50%; cursor: pointer; z-index: 6; transform: translate(0, -50%); width: 40px; height: 40px; transition: all 0.3s ease; /* Tailwind will handle color/bg */ } /* Arrow Positioning Fix (Responsive CSS) */ @media screen and (max-width: 575px) { .cascade-slider_arrow-left { left: 5px; } .cascade-slider_arrow-right { right: 5px; } } @media screen and (min-width: 576px) { .cascade-slider_arrow-left { left: -4%; } .cascade-slider_arrow-right { right: -4%; } } /* Images */ .cascade-slider_slides img { max-width: 150px; height: auto; border-radius: 35px; display: block; transition: filter 1s ease; } /* Tailwind handles the grayscale filter on hover if desired, but keeping the state-based one is better */ .cascade-slider_item:not(.now) img { filter: grayscale(0.95); } /* --- Media Queries (Minimized to only include structural layout changes) --- */ @media screen and (min-width: 414px) { .cascade-slider_container { height: 40vh; } .cascade-slider_slides img { max-width: 200px; } } @media screen and (min-width: 576px) { .cascade-slider_container { height: 60vh; } .cascade-slider_slides img { max-width: 270px; } } @media screen and (min-width: 768px) { .cascade-slider_item.next { transform: translateY(-50%) translateX(-125%) scale(0.6); } .cascade-slider_item.prev { transform: translateY(-50%) translateX(25%) scale(0.6); } .cascade-slider_slides img { max-width: 250px; } } @media screen and (min-width: 991px) { .cascade-slider_item.next { transform: translateY(-50%) translateX(-115%) scale(0.55); z-index: 4; } .cascade-slider_item.prev { transform: translateY(-50%) translateX(15%) scale(0.55); z-index: 4; } .cascade-slider_item.next2 { transform: translateY(-50%) translateX(-150%) scale(0.37); z-index: 1; } .cascade-slider_item.prev2 { transform: translateY(-50%) translateX(50%) scale(0.37); z-index: 2; } .cascade-slider_slides img { max-width: 300px; } .cascade-slider_container { height: 37vh; } } @media screen and (min-width: 1100px) { .cascade-slider_item.next { transform: translateY(-50%) translateX(-130%) scale(0.55); } .cascade-slider_item.prev { transform: translateY(-50%) translateX(30%) scale(0.55); } .cascade-slider_item.next2 { transform: translateY(-50%) translateX(-180%) scale(0.37); } .cascade-slider_item.prev2 { transform: translateY(-50%) translateX(80%) scale(0.37); } .cascade-slider_slides img { max-width: 350px; } } `; // --- Helper Function: Get Slide Classes --- const getSlideClasses = (index: number, activeIndex: number, total: number, visibleCount: 3 | 5): string => { const diff = index - activeIndex; if (diff === 0) return 'now'; if (diff === 1 || diff === -total + 1) return 'next'; if (visibleCount === 5 && (diff === 2 || diff === -total + 2)) return 'next2'; if (diff === -1 || diff === total - 1) return 'prev'; if (visibleCount === 5 && (diff === -2 || diff === total - 2)) return 'prev2'; return ''; }; // --- ThreeDImageCarousel Component Logic --- export const ThreeDImageCarousel: React.FC = ({ slides, itemCount = 5, autoplay = false, delay = 3, pauseOnHover = true, className = '', }) => { const [activeIndex, setActiveIndex] = useState(0); const autoplayIntervalRef = useRef(null); const total = slides.length; const [isDragging, setIsDragging] = useState(false); const [startX, setStartX] = useState(0); const swipeThreshold = 50; const navigate = useCallback((direction: 'next' | 'prev') => { setActiveIndex(current => { if (direction === 'next') { return (current + 1) % total; } else { return (current - 1 + total) % total; } }); }, [total]); const startAutoplay = useCallback(() => { if (autoplay && total > 1) { if (autoplayIntervalRef.current) { clearInterval(autoplayIntervalRef.current); } autoplayIntervalRef.current = window.setInterval(() => { navigate('next'); }, delay * 1000); } }, [autoplay, delay, navigate, total]); const stopAutoplay = useCallback(() => { if (autoplayIntervalRef.current) { clearInterval(autoplayIntervalRef.current); autoplayIntervalRef.current = null; } }, []); useEffect(() => { startAutoplay(); return () => { stopAutoplay(); }; }, [startAutoplay, stopAutoplay]); // Handler to stop autoplay on hover const handleMouseEnter = () => { if (autoplay && pauseOnHover) { stopAutoplay(); } }; // Handler to start autoplay on mouse exit AND handle drag cancellation const handleExit = (e: React.MouseEvent) => { // 1. Autoplay resume logic if (autoplay && pauseOnHover) { startAutoplay(); } // 2. Drag cancellation logic (Equivalent to the removed onMouseLeaveDrag) if (isDragging) { handleEnd(e.clientX); } }; // --- Touch/Mouse Drag Logic --- const handleStart = (clientX: number) => { setIsDragging(true); setStartX(clientX); stopAutoplay(); }; const handleEnd = (clientX: number) => { if (!isDragging) return; const distance = clientX - startX; if (Math.abs(distance) > swipeThreshold) { if (distance < 0) { navigate('next'); // Swipe left (negative distance) -> show next slide } else { navigate('prev'); // Swipe right (positive distance) -> show previous slide } } setIsDragging(false); setStartX(0); // Autoplay is resumed by the useEffect on state change or by handleExit/onMouseUp }; const onMouseDown = (e: React.MouseEvent) => handleStart(e.clientX); const onMouseUp = (e: React.MouseEvent) => { handleEnd(e.clientX); startAutoplay(); // Resume autoplay when mouse button is released }; const onTouchStart = (e: React.TouchEvent) => handleStart(e.touches[0].clientX); const onTouchEnd = (e: React.TouchEvent) => { handleEnd(e.changedTouches[0].clientX); startAutoplay(); // Resume autoplay after touch interaction }; return ( <> {/* 1. EMBEDDED CSS with mobile arrow fix and minimal styling */}
); }; export default ThreeDPerspectiveCard; ``` -------------------------------------------------- ### COMPONENT: 3d-scroll-trigger Category: 3D Elements Description: A high-performance horizontal scroll component with smooth, velocity-based animation. Automatically repeats its children for an infinite 3D-like scrolling effect, ideal for galleries, tickers, or interactive carousels. URL: https://lightswind.com/components/3d-scroll-trigger Import: import { ThreeDScrollTriggerContainer, ThreeDScrollTriggerRow } from '@/components/lightswind/ThreeDScrollTrigger'; Registry URL: https://lightswind.com/r/3d-scroll-trigger.json Install Command: npx lightswind@latest add 3d-scroll-trigger Usage: ```tsx import { ThreeDScrollTriggerContainer, ThreeDScrollTriggerRow } from '@/components/lightswind/ThreeDScrollTrigger';
Item 1
Item 2
Item 3
``` Source Code: ```tsx // ThreeDScrollTrigger.tsx "use client"; import React, { useRef, useEffect, useMemo, useContext, } from "react"; import { cn } from "@/components/lib/utils"; /* ------------------------- Utility: wrap (backward compatibility) ------------------------- */ export const wrap = (min: number, max: number, v: number) => { const rangeSize = max - min; return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min; }; /* ----------------------------------- Context for passive scroll velocity ----------------------------------- */ interface ScrollVelocityContextType { getVelocity: () => number; } const ThreeDScrollTriggerContext = React.createContext({ getVelocity: () => 0, }); /* -------------------------- Container that tracks scroll velocity passively without re-renders -------------------------- */ export function ThreeDScrollTriggerContainer({ children, className, ...props }: React.HTMLAttributes) { const targetVelocityRef = useRef(0); const lastScrollY = useRef(0); const lastTime = useRef(0); const decayTimeoutRef = useRef | null>(null); useEffect(() => { lastScrollY.current = window.scrollY; lastTime.current = performance.now(); const handleScroll = () => { const now = performance.now(); const dt = now - lastTime.current; if (dt <= 0) return; const currentScrollY = window.scrollY; const deltaY = currentScrollY - lastScrollY.current; // Calculate instant velocity in px/second (clamped to realistic range [-2500, 2500]) const instantVelocity = (deltaY / Math.max(6, dt)) * 1000; targetVelocityRef.current = Math.max(-2500, Math.min(2500, instantVelocity)); lastScrollY.current = currentScrollY; lastTime.current = now; // When active continuous scrolling ceases, decay target velocity to zero if (decayTimeoutRef.current) clearTimeout(decayTimeoutRef.current); decayTimeoutRef.current = setTimeout(() => { targetVelocityRef.current = 0; }, 50); }; window.addEventListener("scroll", handleScroll, { passive: true }); return () => { window.removeEventListener("scroll", handleScroll); if (decayTimeoutRef.current) clearTimeout(decayTimeoutRef.current); }; }, []); const contextValue = useMemo( () => ({ getVelocity: () => targetVelocityRef.current, }), [] ); return (
{children}
); } /* -------------------------- Props -------------------------- */ export interface ThreeDScrollTriggerRowProps extends React.HTMLAttributes { children: React.ReactNode; baseVelocity?: number; // Speed multiplier (e.g. 5, 6) direction?: 1 | -1; resetIntervalMs?: number; tiltEffect?: boolean; // Dynamic 3D card tilt angle during scroll } /* -------------------------- High-FPS GPU-Accelerated 3D Row -------------------------- */ export function ThreeDScrollTriggerRow({ children, baseVelocity = 5, direction = 1, tiltEffect = true, className, ...props }: ThreeDScrollTriggerRowProps) { const context = useContext(ThreeDScrollTriggerContext); const containerRef = useRef(null); const trackRef = useRef(null); const singleBlockRef = useRef(null); const xRef = useRef(0); const unitWidthRef = useRef(0); const smoothVelocityRef = useRef(0); const smoothTiltRef = useRef(0); const isInViewRef = useRef(false); const rafIdRef = useRef(null); const startAnimationRef = useRef<(() => void) | null>(null); // Measure single block width with ResizeObserver & sub-pixel precision useEffect(() => { const measure = () => { if (singleBlockRef.current) { const rect = singleBlockRef.current.getBoundingClientRect(); unitWidthRef.current = rect.width || singleBlockRef.current.offsetWidth || 0; } }; measure(); let ro: ResizeObserver | null = null; if (typeof ResizeObserver !== "undefined" && singleBlockRef.current) { ro = new ResizeObserver(measure); ro.observe(singleBlockRef.current); } window.addEventListener("resize", measure, { passive: true }); return () => { ro?.disconnect(); window.removeEventListener("resize", measure); }; }, [children]); // Viewport intersection observer: completely halt rAF when scrolled away useEffect(() => { const el = containerRef.current; if (!el || typeof IntersectionObserver === "undefined") { isInViewRef.current = true; return; } const io = new IntersectionObserver( ([entry]) => { const wasInView = isInViewRef.current; isInViewRef.current = entry && entry.isIntersecting; if (isInViewRef.current && !wasInView && startAnimationRef.current) { startAnimationRef.current(); } }, { rootMargin: "250px" } ); io.observe(el); return () => io.disconnect(); }, []); // Ultra-Smooth Direct-GPU Animation Loop (120 FPS, 0 React re-renders) useEffect(() => { let lastTime = performance.now(); const animate = (now: number) => { if (!isInViewRef.current) { // Completely halt rAF scheduling when not in view return; } // Safe delta time clamped to avoid jumps after tab switch / pause const dt = Math.min(0.04, Math.max(0.001, (now - lastTime) / 1000)); lastTime = now; // 1. Smoothly interpolate velocity using exponential damping (frame-rate independent) const targetVelocity = context?.getVelocity() || 0; const lerpFactor = 1 - Math.exp(-12 * dt); smoothVelocityRef.current += (targetVelocity - smoothVelocityRef.current) * lerpFactor; const unitWidth = unitWidthRef.current; if (unitWidth > 0) { // 2. Base cruising speed (~26px/sec per baseVelocity unit) const baseSpeed = Math.abs(baseVelocity) * 26; // 3. Dynamic scroll velocity boost const scrollBoost = Math.abs(smoothVelocityRef.current) * 0.45; // 4. Direction handling: // When scrolling down (velocity >= 0): row cruises and accelerates in its configured direction // When scrolling up towards top (velocity < 0): row smoothly reverses and accelerates backwards const isScrollingUp = smoothVelocityRef.current < -30; const scrollDirection = isScrollingUp ? -1 : 1; const effectiveDirection = direction * scrollDirection; // 5. Compute net frame movement const currentSpeed = effectiveDirection * (baseSpeed + scrollBoost); const moveDelta = currentSpeed * dt; xRef.current += moveDelta; // 6. Seamless continuous modulo wrapping (works flawlessly for positive and negative values) xRef.current = ((xRef.current % unitWidth) + unitWidth) % unitWidth; // 7. Dynamic 3D card tilt angle (reacts smoothly to scroll velocity & direction) let tiltTransform = ""; if (tiltEffect) { const targetTilt = Math.max(-4.5, Math.min(4.5, (currentSpeed / 200) * 1.8)); const tiltLerp = 1 - Math.exp(-14 * dt); smoothTiltRef.current += (targetTilt - smoothTiltRef.current) * tiltLerp; tiltTransform = ` skewX(${-smoothTiltRef.current}deg)`; } // 8. Direct hardware-accelerated transform on GPU compositor if (trackRef.current) { trackRef.current.style.transform = `translate3d(${-xRef.current}px, 0, 0)${tiltTransform}`; } } rafIdRef.current = requestAnimationFrame(animate); }; startAnimationRef.current = () => { lastTime = performance.now(); if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current); rafIdRef.current = requestAnimationFrame(animate); }; if (isInViewRef.current) { startAnimationRef.current(); } return () => { startAnimationRef.current = null; if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current); }; }, [baseVelocity, direction, tiltEffect, context]); const childrenArray = useMemo( () => React.Children.toArray(children), [children] ); return (
{/* Set 1: Measured Reference Block */}
{childrenArray}
{/* Set 2: Seamless Clone */} {/* Set 3: Seamless Clone */} {/* Set 4: Seamless Clone for ultra-wide displays */}
); } export default ThreeDScrollTriggerRow; ``` -------------------------------------------------- ### COMPONENT: 3d-smokey-frame Category: 3D Elements Description: An interactive WebGL volumetric noise smokey frame shader featuring customizable frame width, wave propagation speed, color palettes, atmospheric glow, and cursor-reactive turbulence displacement. URL: https://lightswind.com/components/3d-smokey-frame Import: import ThreeDSmokeyFrame from "@/components/lightswind/3d-smokey-frame"; Registry URL: https://lightswind.com/r/3d-smokey-frame.json Install Command: npx lightswind@latest add 3d-smokey-frame Usage: ```tsx import ThreeDSmokeyFrame from "@/components/lightswind/3d-smokey-frame"; export default function Example() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useRef, useEffect, useCallback, PropsWithChildren, CSSProperties, forwardRef, useImperativeHandle } from "react"; import { useTheme } from "next-themes"; import { cn } from "@/components/lib/utils"; export interface ThreeDSmokeyFrameProps extends PropsWithChildren { /** Color of the animated smokey frame (Hex or RGB/RGBA, default: "#00F5FF" Cosmic Cyan) */ frameColor?: string; /** Background interior base color (used when transparentBg is false) */ frameBgColor?: string; /** Whether the interior background is completely transparent (default: true) */ transparentBg?: boolean; /** Normalized frame width from edge toward center (0.01 - 0.50, default: 0.30) */ frameWidth?: number; /** Animation wave and smoke propagation speed multiplier (default: 0.15) */ speed?: number; /** Edge falloff curve exponent (higher values make the frame edge sharper, default: 6.0) */ falloff?: number; /** Granularity and density of the procedural noise smoke (default: 3.0) */ noiseScale?: number; /** Amount of noise turbulence modulating the frame (0.0 - 1.0, default: 1.0) */ noiseStrength?: number; /** Brightness and emission intensity of the smoke (default: 1.2) */ intensity?: number; /** Gamma curve contrast adjustment (default: 2.0) */ gamma?: number; /** Overall canvas opacity (0.0 - 1.0, default: 1.0) */ opacity?: number; /** Whether cursor movement dynamically displaces the smokey field (default: true) */ interactive?: boolean; /** Enable external atmospheric colored glow behind the frame container (default: true) */ glow?: boolean; /** Blur radius for external atmospheric glow in pixels (default: 36) */ glowBlur?: number; /** Opacity for external ambient glow (default: 0.45) */ glowOpacity?: number; /** Border radius for the frame container (default: "16px") */ radius?: string | number; /** Optional class name for the outer container */ className?: string; /** Optional class name for the canvas element */ canvasClassName?: string; /** Optional inline styles */ style?: CSSProperties; /** Maximum device pixel ratio to use for rendering (default: 2) */ dpr?: number; } export interface ThreeDSmokeyFrameHandle { getCanvas: () => HTMLCanvasElement | null; getGL: () => WebGLRenderingContext | null; } /** Utility to parse Hex or RGB strings to normalized [r, g, b] float vectors */ function parseColorToRgb(color: string, fallback: [number, number, number] = [0, 0.96, 1]): [number, number, number] { if (!color) return fallback; const clean = color.trim(); if (clean.startsWith("#")) { let hex = clean.replace("#", ""); if (hex.length === 3) { hex = hex.split("").map((c) => c + c).join(""); } const num = parseInt(hex, 16); if (isNaN(num)) return fallback; return [ ((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255, ]; } const match = clean.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (match) { return [ parseInt(match[1], 10) / 255, parseInt(match[2], 10) / 255, parseInt(match[3], 10) / 255, ]; } return fallback; } const VERTEX_SHADER = ` attribute vec2 a_position; varying vec2 v_uv; void main() { v_uv = (a_position + 1.0) * 0.5; gl_Position = vec4(a_position, 0.0, 1.0); } `; const FRAGMENT_SHADER = ` precision highp float; varying vec2 v_uv; uniform vec2 u_resolution; uniform float u_time; uniform float u_speed; uniform float u_frameWidth; uniform float u_falloff; uniform float u_noiseScale; uniform float u_noiseStrength; uniform float u_intensity; uniform float u_gamma; uniform float u_opacity; uniform vec3 u_frameColor; uniform vec3 u_frameBgColor; uniform float u_transparentBg; uniform vec2 u_mouse; uniform float u_isHovered; // Simplex 2D noise generator vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); } float snoise(vec2 v){ const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); vec2 i = floor(v + dot(v, C.yy) ); vec2 x0 = v - i + dot(i, C.xx); vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); vec4 x12 = x0.xyxy + C.xxzz; x12.xy -= i1; i = mod(i, 289.0); vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 )); vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); m = m*m ; m = m*m ; vec3 x = 2.0 * fract(p * C.www) - 1.0; vec3 h = abs(x) - 0.5; vec3 ox = floor(x + 0.5); vec3 a0 = x - ox; m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); vec3 g; g.x = a0.x * x0.x + h.x * x0.y; g.yz = a0.yz * x12.xz + h.yz * x12.yw; return 130.0 * dot(m, g); } // 5-Octave Fractional Brownian Motion for lush smoke tendrils float fbm(vec2 p) { float total = 0.0; float amp = 0.5; float freq = 1.0; for(int i = 0; i < 5; i++) { total += snoise(p * freq) * amp; freq *= 2.02; amp *= 0.5; } return total; } void main() { vec2 uv = v_uv; float aspect = u_resolution.x / u_resolution.y; // Aspect ratio correction for noise sampling vec2 noiseUV = uv; if (aspect > 1.0) { noiseUV.x *= aspect; } else { noiseUV.y /= aspect; } // Interactive mouse turbulence displacement if (u_isHovered > 0.0) { vec2 mouseUV = u_mouse; if (aspect > 1.0) mouseUV.x *= aspect; else mouseUV.y /= aspect; float dMouse = distance(noiseUV, mouseUV); float mouseInfluence = smoothstep(0.6, 0.0, dMouse); noiseUV += (noiseUV - mouseUV) * mouseInfluence * 0.12 * u_isHovered; } // Animated volumetric noise field float t = u_time * u_speed; float noise1 = fbm(noiseUV * u_noiseScale + vec2(t * 0.45, t * 0.28)); float noise2 = fbm(noiseUV * (u_noiseScale * 1.5) - vec2(t * 0.32, -t * 0.4)); float combinedNoise = (noise1 * 0.65 + noise2 * 0.35 + 1.0) * 0.5; // Distance calculation from all 4 boundaries (0 at boundary, 0.5 at center) vec2 distToEdge = min(uv, 1.0 - uv); float minAxisDist = min(distToEdge.x, distToEdge.y); // Normalize distance based on the frameWidth parameter float frameDist = clamp(minAxisDist / max(u_frameWidth, 0.0001), 0.0, 1.0); // Invert so frame edge = 1.0, interior core = 0.0 float edgeStrength = 1.0 - frameDist; edgeStrength = pow(edgeStrength, u_falloff); // Modulate edge with procedural smoke noise float modulatedFrame = mix(edgeStrength, edgeStrength * combinedNoise, u_noiseStrength); // Apply intensity multiplier and gamma curve for rich contrast float finalGlow = pow(modulatedFrame * u_intensity, u_gamma); finalGlow = clamp(finalGlow, 0.0, 1.0); // Render with transparent background or blended solid background if (u_transparentBg > 0.5) { float alpha = finalGlow * u_opacity; gl_FragColor = vec4(u_frameColor, alpha); } else { vec3 finalColor = mix(u_frameBgColor, u_frameColor, finalGlow); gl_FragColor = vec4(finalColor, u_opacity); } } `; function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null { const shader = gl.createShader(type); if (!shader) return null; gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error("Shader compile error:", gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } export const ThreeDSmokeyFrame = forwardRef(({ children, frameColor = "#00F5FF", frameBgColor, transparentBg = true, frameWidth = 0.30, speed = 0.15, falloff = 6.0, noiseScale = 3.0, noiseStrength = 1.0, intensity = 1.2, gamma = 2.0, opacity = 1.0, interactive = true, glow = true, glowBlur = 36, glowOpacity = 0.45, radius = "16px", className, canvasClassName, style, dpr = 2, }, ref) => { const containerRef = useRef(null); const canvasRef = useRef(null); const glRef = useRef(null); const animFrameRef = useRef(null); const isVisibleRef = useRef(true); const { resolvedTheme, theme } = useTheme(); const isLightMode = resolvedTheme === "light" || theme === "light"; const effectiveBgColor = frameBgColor ?? (isLightMode ? "#ffffff" : "#08080a"); const mousePosRef = useRef<{ x: number; y: number }>({ x: 0.5, y: 0.5 }); const isHoveredRef = useRef(0); const startTimeRef = useRef(performance.now()); const parsedRadius = typeof radius === "number" ? `${radius}px` : radius; useImperativeHandle(ref, () => ({ getCanvas: () => canvasRef.current, getGL: () => glRef.current, })); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const gl = canvas.getContext("webgl", { alpha: true, antialias: true, depth: false, preserveDrawingBuffer: false, }); if (!gl) { console.warn("WebGL not supported for ThreeDSmokeyFrame"); return; } glRef.current = gl; gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); const vs = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER); const fs = createShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER); if (!vs || !fs) return; const program = gl.createProgram(); if (!program) return; gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error("Program link error:", gl.getProgramInfoLog(program)); return; } gl.useProgram(program); // Quad Geometry Buffers const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1, ]), gl.STATIC_DRAW ); const positionLocation = gl.getAttribLocation(program, "a_position"); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); // Uniform Locations const uniforms = { resolution: gl.getUniformLocation(program, "u_resolution"), time: gl.getUniformLocation(program, "u_time"), speed: gl.getUniformLocation(program, "u_speed"), frameWidth: gl.getUniformLocation(program, "u_frameWidth"), falloff: gl.getUniformLocation(program, "u_falloff"), noiseScale: gl.getUniformLocation(program, "u_noiseScale"), noiseStrength: gl.getUniformLocation(program, "u_noiseStrength"), intensity: gl.getUniformLocation(program, "u_intensity"), gamma: gl.getUniformLocation(program, "u_gamma"), opacity: gl.getUniformLocation(program, "u_opacity"), frameColor: gl.getUniformLocation(program, "u_frameColor"), frameBgColor: gl.getUniformLocation(program, "u_frameBgColor"), transparentBg: gl.getUniformLocation(program, "u_transparentBg"), mouse: gl.getUniformLocation(program, "u_mouse"), isHovered: gl.getUniformLocation(program, "u_isHovered"), }; const handleResize = () => { if (!canvas || !gl) return; const targetDpr = Math.min(window.devicePixelRatio || 1, dpr); const displayWidth = Math.round(canvas.clientWidth * targetDpr); const displayHeight = Math.round(canvas.clientHeight * targetDpr); if (canvas.width !== displayWidth || canvas.height !== displayHeight) { canvas.width = Math.max(1, displayWidth); canvas.height = Math.max(1, displayHeight); gl.viewport(0, 0, canvas.width, canvas.height); } }; handleResize(); const resizeObserver = new ResizeObserver(() => { handleResize(); }); resizeObserver.observe(canvas); const intersectionObserver = new IntersectionObserver( ([entry]) => { isVisibleRef.current = entry.isIntersecting; }, { threshold: 0.05 } ); intersectionObserver.observe(canvas); let currentHover = 0; const render = () => { if (isVisibleRef.current && gl && canvas) { handleResize(); const time = (performance.now() - startTimeRef.current) * 0.001; const fColor = parseColorToRgb(frameColor, [0, 0.96, 1]); const bColor = parseColorToRgb(effectiveBgColor, [0.04, 0.04, 0.04]); // Smooth hover transition const targetHover = isHoveredRef.current; currentHover += (targetHover - currentHover) * 0.1; gl.uniform2f(uniforms.resolution, canvas.width, canvas.height); gl.uniform1f(uniforms.time, time); gl.uniform1f(uniforms.speed, speed); gl.uniform1f(uniforms.frameWidth, frameWidth); gl.uniform1f(uniforms.falloff, falloff); gl.uniform1f(uniforms.noiseScale, noiseScale); gl.uniform1f(uniforms.noiseStrength, noiseStrength); gl.uniform1f(uniforms.intensity, intensity); gl.uniform1f(uniforms.gamma, gamma); gl.uniform1f(uniforms.opacity, opacity); gl.uniform3f(uniforms.frameColor, fColor[0], fColor[1], fColor[2]); gl.uniform3f(uniforms.frameBgColor, bColor[0], bColor[1], bColor[2]); gl.uniform1f(uniforms.transparentBg, transparentBg ? 1.0 : 0.0); gl.uniform2f(uniforms.mouse, mousePosRef.current.x, mousePosRef.current.y); gl.uniform1f(uniforms.isHovered, currentHover); gl.drawArrays(gl.TRIANGLES, 0, 6); } animFrameRef.current = requestAnimationFrame(render); }; animFrameRef.current = requestAnimationFrame(render); return () => { if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current); resizeObserver.disconnect(); intersectionObserver.disconnect(); if (gl) { gl.deleteProgram(program); gl.deleteShader(vs); gl.deleteShader(fs); gl.deleteBuffer(positionBuffer); } }; }, [ frameColor, effectiveBgColor, transparentBg, frameWidth, speed, falloff, noiseScale, noiseStrength, intensity, gamma, opacity, dpr, ]); const handleMouseMove = useCallback((e: React.MouseEvent) => { if (!interactive || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; const y = 1.0 - (e.clientY - rect.top) / rect.height; // Invert for WebGL UV coords mousePosRef.current = { x, y }; }, [interactive]); const handleMouseEnter = useCallback(() => { if (interactive) isHoveredRef.current = 1.0; }, [interactive]); const handleMouseLeave = useCallback(() => { if (interactive) isHoveredRef.current = 0.0; }, [interactive]); return (
{/* Ambient Atmosphere Glow Backdrop */} {glow && (
)} {/* WebGL Canvas Shader Output */} {/* Slotted Children Content Layer */} {children && (
{children}
)}
); }); ThreeDSmokeyFrame.displayName = "ThreeDSmokeyFrame"; export default ThreeDSmokeyFrame; ``` -------------------------------------------------- ### COMPONENT: 3d-image-pageflip Category: 3D Elements Description: An interactive 3D book and magazine page flip component featuring dynamic spine shifting, realistic lighting crease shadows, hover peeking, and leaf stacking. URL: https://lightswind.com/components/3d-image-pageflip Import: import ThreeDImagePageflip, { PageFlipLeaf } from "@/components/lightswind/3d-image-pageflip"; Registry URL: https://lightswind.com/r/3d-image-pageflip.json Install Command: npx lightswind@latest add 3d-image-pageflip Usage: ```tsx import ThreeDImagePageflip, { PageFlipLeaf } from "@/components/lightswind/3d-image-pageflip"; const pages: PageFlipLeaf[] = [ { frontImage: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&w=800&q=80", frontTitle: "Villa Solarium", frontSubtitle: "Horizon Pool & Architecture", frontBadge: "Cover", backTitle: "Minimal Horizon", backSubtitle: "Geometric Water Pavilion", backBadge: "Plate 01", }, { frontImage: "https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=800&q=80", frontTitle: "Apex Structure", frontSubtitle: "Parametric Glass Facade", frontBadge: "Plate 02", backTitle: "Glass Skyline", backSubtitle: "Monolith Tower", backBadge: "Plate 03", }, ]; export default function Example() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useState, useEffect, useCallback, CSSProperties, forwardRef, useImperativeHandle } from "react"; import { cn } from "@/components/lib/utils"; import { ChevronLeft, ChevronRight, RotateCcw } from "lucide-react"; export interface PageFlipLeaf { id?: string | number; frontImage: string; backImage: string; frontTitle?: string; backTitle?: string; frontSubtitle?: string; backSubtitle?: string; frontBadge?: string; backBadge?: string; } export interface ThreeDImagePageflipProps { /** Array of page leaves, each containing front and back images & metadata */ pages?: PageFlipLeaf[]; /** Default turned page count (0 = closed book on cover) */ defaultTurnedIndex?: number; /** Controlled turned page count */ turnedIndex?: number; /** Callback fired when page flip changes */ onPageChange?: (turnedCount: number, totalLeaves: number) => void; /** Width of a single page in pixels (default: 230) */ pageWidth?: number; /** Height of a single page in pixels (default: 330) */ pageHeight?: number; /** 3D perspective depth in pixels (default: 1300) */ perspective?: number; /** Maximum hover peek angle in degrees (default: 14) */ peekAngle?: number; /** Total turn angle in degrees (default: 180) */ turnAngle?: number; /** Transition flip animation duration in seconds (default: 0.65) */ duration?: number; /** Easing curve for flip animation (default: "cubic-bezier(0.4, 0, 0.2, 1)") */ easing?: string; /** Shadow intensity factor (0.0 to 1.0, default: 0.45) */ shadowIntensity?: number; /** Dynamically shift spine horizontally when book is open to center the 2-page spread (default: true) */ spineShift?: boolean; /** Border radius for pages (default: "10px") */ radius?: string | number; /** Enable page numbering tags (default: true) */ showPageNumbers?: boolean; /** Enable outer book leather spine binding (default: true) */ showSpineBinding?: boolean; /** Accent glow color for active elements (default: "#00F5FF") */ accentColor?: string; /** Enable automatic page flipping (default: false) */ autoplay?: boolean; /** Autoplay interval in milliseconds (default: 3500) */ autoplayInterval?: number; /** Pause autoplay on hover (default: true) */ pauseOnHover?: boolean; /** Enable interactive click on pages to flip (default: true) */ interactive?: boolean; /** Enable navigation buttons (default: true) */ showControls?: boolean; /** Optional container class name */ className?: string; /** Optional container inline style */ style?: CSSProperties; } export interface ThreeDImagePageflipHandle { next: () => void; prev: () => void; reset: () => void; goTo: (index: number) => void; getTurnedCount: () => number; getTotalLeaves: () => number; } const DEFAULT_PAGES: PageFlipLeaf[] = [ { id: 1, frontImage: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&w=800&q=80", frontTitle: "Villa Solarium", frontSubtitle: "Horizon Pool & Architecture", frontBadge: "Cover", backTitle: "Minimal Horizon", backSubtitle: "Geometric Water Pavilion", backBadge: "Plate 01", }, { id: 2, frontImage: "https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=800&q=80", frontTitle: "Apex Structure", frontSubtitle: "Parametric Glass Facade", frontBadge: "Plate 02", backTitle: "Glass Skyline", backSubtitle: "Monolith Metropolitan Tower", backBadge: "Plate 03", }, { id: 3, frontImage: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1511818966892-d7d671e672a2?auto=format&fit=crop&w=800&q=80", frontTitle: "Emerald Cascade", frontSubtitle: "Alpine Mist & Forest Ridge", frontBadge: "Plate 04", backTitle: "Nordic Pavilion", backSubtitle: "Natural Timber Canopy", backBadge: "Plate 05", }, { id: 4, frontImage: "https://images.unsplash.com/photo-1518780664697-55e3ad937233?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1507652313519-d4e9174996dd?auto=format&fit=crop&w=800&q=80", frontTitle: "Warm Solarium", frontSubtitle: "Sunlight & Interior Loft", frontBadge: "Plate 06", backTitle: "Zen Courtyard", backSubtitle: "Brutalist Stone Water Feature", backBadge: "Plate 07", }, { id: 5, frontImage: "https://images.unsplash.com/photo-1486325212027-8081e485255e?auto=format&fit=crop&w=800&q=80", backImage: "https://images.unsplash.com/photo-1479839672679-a46483c0e7c8?auto=format&fit=crop&w=800&q=80", frontTitle: "Cyber Metropolis", frontSubtitle: "Urban Geometric Skyline", frontBadge: "Plate 08", backTitle: "Monolith Curve", backSubtitle: "Brutalist Concrete Finish", backBadge: "Endplate", }, ]; export const ThreeDImagePageflip = forwardRef(({ pages = DEFAULT_PAGES, defaultTurnedIndex = 0, turnedIndex: controlledTurnedIndex, onPageChange, pageWidth = 230, pageHeight = 330, perspective = 1300, peekAngle = 14, turnAngle = 180, duration = 0.65, easing = "cubic-bezier(0.4, 0, 0.2, 1)", shadowIntensity = 0.45, spineShift = true, radius = "10px", showPageNumbers = true, showSpineBinding = true, accentColor = "#00F5FF", autoplay = false, autoplayInterval = 3500, pauseOnHover = true, interactive = true, showControls = true, className, style, }, ref) => { const [internalTurned, setInternalTurned] = useState(defaultTurnedIndex); const [isHovered, setIsHovered] = useState(false); const [peekingIndex, setPeekingIndex] = useState(null); const totalLeaves = pages.length; const currentTurned = controlledTurnedIndex !== undefined ? controlledTurnedIndex : internalTurned; const isOpen = currentTurned > 0 && currentTurned < totalLeaves; const parsedRadius = typeof radius === "number" ? `${radius}px` : radius; const setTurned = useCallback((newCount: number) => { const clamped = Math.max(0, Math.min(newCount, totalLeaves)); if (controlledTurnedIndex === undefined) { setInternalTurned(clamped); } if (onPageChange) { onPageChange(clamped, totalLeaves); } }, [controlledTurnedIndex, totalLeaves, onPageChange]); const flipNext = useCallback(() => { if (currentTurned < totalLeaves) { setTurned(currentTurned + 1); } }, [currentTurned, totalLeaves, setTurned]); const flipPrev = useCallback(() => { if (currentTurned > 0) { setTurned(currentTurned - 1); } }, [currentTurned, setTurned]); const resetBook = useCallback(() => { setTurned(0); }, [setTurned]); useImperativeHandle(ref, () => ({ next: flipNext, prev: flipPrev, reset: resetBook, goTo: (idx) => setTurned(idx), getTurnedCount: () => currentTurned, getTotalLeaves: () => totalLeaves, })); // Autoplay Timer useEffect(() => { if (!autoplay || (pauseOnHover && isHovered) || totalLeaves <= 1) return; const timer = setInterval(() => { setInternalTurned((prev) => (prev >= totalLeaves ? 0 : prev + 1)); }, autoplayInterval); return () => clearInterval(timer); }, [autoplay, autoplayInterval, pauseOnHover, isHovered, totalLeaves]); // Keyboard Navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowRight") flipNext(); if (e.key === "ArrowLeft") flipPrev(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [flipNext, flipPrev]); const handleLeafClick = (index: number) => { if (!interactive) return; if (index === currentTurned) { // Click unturned top page -> flip forward flipNext(); } else if (index === currentTurned - 1) { // Click turned top left page -> flip backward flipPrev(); } }; return (
setIsHovered(true)} onMouseLeave={() => { setIsHovered(false); setPeekingIndex(null); }} > {/* 3D Book Viewport Stage */}
{/* 3D Book Container */}
{/* Spine Shadow & Binding Crease */} {showSpineBinding && (
)} {/* Ground Ambience Drop Shadow underneath the book */}
{/* Book Leaves Stacking Loop */} {pages.map((leaf, index) => { const isTurned = index < currentTurned; const isCanPeek = index === currentTurned; const isPeeking = peekingIndex === index; // Calculate Z-Index: turned leaves stack forward on left, unturned leaves stack backward on right const zIndex = isTurned ? index + 1 : totalLeaves - index; // Rotation Angle let leafRotation = isTurned ? -turnAngle : 0; if (!isTurned && isPeeking) { leafRotation = -peekAngle; } return (
handleLeafClick(index)} onMouseEnter={() => { if (isCanPeek) setPeekingIndex(index); }} onMouseLeave={() => { if (peekingIndex === index) setPeekingIndex(null); }} className={cn( "absolute inset-0 origin-left cursor-pointer", interactive ? "cursor-pointer" : "pointer-events-none" )} style={{ transformStyle: "preserve-3d", transition: `transform ${duration}s ${easing}`, transform: `rotateY(${leafRotation}deg)`, zIndex, borderRadius: parsedRadius, }} > {/* FRONT FACE (Visible when page is on the right) */}
{leaf.frontTitle {/* Spine crease shadow overlay for 3D depth */}
{/* Bottom Vignette & Metadata */}
{leaf.frontBadge && ( {leaf.frontBadge} )} {showPageNumbers && ( {index * 2 + 1} )}
{leaf.frontTitle && (

{leaf.frontTitle}

)} {leaf.frontSubtitle && (

{leaf.frontSubtitle}

)}
{/* BACK FACE (Visible when page is turned to the left) */}
{leaf.backTitle {/* Spine crease shadow overlay for turned back-face */}
{/* Bottom Vignette & Metadata */}
{leaf.backBadge && ( {leaf.backBadge} )} {showPageNumbers && ( {index * 2 + 2} )}
{leaf.backTitle && (

{leaf.backTitle}

)} {leaf.backSubtitle && (

{leaf.backSubtitle}

)}
); })}
{/* Book Controls & Page Progress Toolbar */} {showControls && (
{currentTurned} / {totalLeaves} leaves
)}
); }); ThreeDImagePageflip.displayName = "ThreeDImagePageflip"; export default ThreeDImagePageflip; ``` -------------------------------------------------- ### COMPONENT: 3d-slider Category: 3D Elements Description: A responsive, 3D image/content slider with a smooth, cascaded stacking effect. It uses Framer Motion for the complex 3D transforms (translateX, translateY, rotate) and handles interactivity via global document event listeners for mouse wheel, drag, and touch gestures. The component is styled using Tailwind CSS and is fully self-contained. URL: https://lightswind.com/components/3d-slider Import: import ThreeDSlider from './ThreeDSlider'; // Adjust path as needed Registry URL: https://lightswind.com/r/3d-slider.json Install Command: npx lightswind@latest add 3d-slider Usage: ```tsx // 1. Define the items data const sliderItems = [ { title: "First Item", num: "01", imageUrl: "/images/image1.jpg", data: { id: 1 } }, { title: "Second Item", num: "02", imageUrl: "/images/image2.jpg", data: { id: 2 } }, { title: "Third Item", num: "03", imageUrl: "/images/image3.jpg", data: { id: 3 } }, { title: "Fourth Item", num: "04", imageUrl: "/images/image4.jpg", data: { id: 4 } }, { title: "Fifth Item", num: "05", imageUrl: "/images/image5.jpg", data: { id: 5 } }, ]; // 2. Default Usage // 3. Custom Interaction Speeds and Click Handler const handleItemClick = (item, index) => { console.log(`Clicked item ${item.num}: ${item.title} at index ${index}`); }; ``` Source Code: ```tsx "use client"; import React, { useEffect, useRef, CSSProperties } from "react"; // --- Type Definitions --- export interface SliderItemData { title: string; num: string; imageUrl: string; data?: any; } interface ThreeDSliderProps { items: SliderItemData[]; speedWheel?: number; speedDrag?: number; containerStyle?: CSSProperties; className?: string; onItemClick?: (item: SliderItemData, index: number) => void; } // ─── Pure DOM card renderer (no React re-renders in the hot path) ──────────── function createCard(item: SliderItemData, index: number): HTMLDivElement { const card = document.createElement("div"); card.className = "absolute top-1/2 left-1/2 cursor-pointer select-none rounded-2xl shadow-2xl bg-zinc-900 overflow-hidden border border-white/10"; card.style.cssText = ` --w: clamp(180px, 28vw, 280px); --h: clamp(240px, 36vw, 380px); width: var(--w); height: var(--h); margin-top: calc(var(--h) / -2); margin-left: calc(var(--w) / -2); will-change: transform, opacity; contain: layout style paint; transition: none; display: block; `; // inner wrapper const inner = document.createElement("div"); inner.style.cssText = "position:absolute;inset:0;z-index:10;"; inner.dataset.inner = "true"; // gradient overlay const grad = document.createElement("div"); grad.style.cssText = "position:absolute;inset:0;z-index:10;background:linear-gradient(to bottom,rgba(0,0,0,.4) 0%,transparent 50%,rgba(0,0,0,.8) 100%);"; // number const num = document.createElement("div"); num.style.cssText = "position:absolute;z-index:20;color:rgba(255,255,255,.9);font-weight:900;top:12px;left:20px;font-size:clamp(28px,6vw,64px);letter-spacing:-0.04em;opacity:.8;"; num.textContent = item.num; // title const title = document.createElement("div"); title.style.cssText = "position:absolute;z-index:20;color:#fff;font-weight:700;bottom:20px;left:20px;font-size:clamp(18px,2.5vw,26px);letter-spacing:-0.02em;text-shadow:0 2px 8px rgba(0,0,0,.5);"; title.textContent = item.title; // image const img = document.createElement("img"); img.src = item.imageUrl; img.alt = item.title; img.loading = index < 3 ? "eager" : "lazy"; img.decoding = "async"; img.style.cssText = "width:100%;height:100%;object-fit:cover;pointer-events:none;display:block;"; inner.appendChild(grad); inner.appendChild(num); inner.appendChild(title); inner.appendChild(img); card.appendChild(inner); return card; } // ─── Main Component ─────────────────────────────────────────────────────────── const ThreeDSlider: React.FC = ({ items, speedWheel = 0.04, speedDrag = -0.15, containerStyle = {}, className = "", onItemClick, }) => { const containerRef = useRef(null); // Keep callback ref stable without causing re-renders const onItemClickRef = useRef(onItemClick); useEffect(() => { onItemClickRef.current = onItemClick; }, [onItemClick]); useEffect(() => { const container = containerRef.current; if (!container || items.length === 0) return; const numItems = items.length; // ── State (all in plain refs, zero React state) ── let progress = 50; let targetProgress = 50; let isDown = false; let startX = 0; let rafId: number | null = null; let isAnimating = false; // ── Build DOM cards ── const cards: HTMLDivElement[] = items.map((item, i) => createCard(item, i)); cards.forEach((card, i) => { card.addEventListener("click", () => { const denom = numItems > 1 ? numItems - 1 : 1; targetProgress = (i / denom) * 100; startLoop(); onItemClickRef.current?.(items[i], i); }, { passive: true }); container.appendChild(card); }); // ── Transform cache to skip identical writes ── const cache: { tx: string; ty: string; rot: string; z: string; op: string }[] = items.map(() => ({ tx: "", ty: "", rot: "", z: "", op: "" })); // ── Core update — pure math, direct DOM writes ── function update() { // Responsive lerp: fast while dragging, smooth when releasing const lerpFactor = isDown ? 1 : 0.1; progress += (targetProgress - progress) * lerpFactor; const clamped = Math.max(0, Math.min(100, progress)); const activeFloat = (clamped / 100) * (numItems - 1); const denom = numItems > 1 ? numItems - 1 : 1; for (let i = 0; i < numItems; i++) { const card = cards[i]; if (!card) continue; const ratio = (i - activeFloat) / denom; const tx = (ratio * 750).toFixed(2); const ty = (ratio * 180).toFixed(2); const rot = (ratio * 110).toFixed(2); const dist = Math.abs(i - activeFloat); const z = numItems - dist; const op = Math.max(0, Math.min(1, (z / numItems) * 3 - 1.8)).toFixed(2); const zStr = Math.round(z * 10).toString(); const c = cache[i]; if (c.tx !== tx || c.ty !== ty || c.rot !== rot) { card.style.transform = `translate3d(${tx}%, ${ty}%, 0) rotate(${rot}deg)`; c.tx = tx; c.ty = ty; c.rot = rot; } if (c.z !== zStr) { card.style.zIndex = zStr; c.z = zStr; } if (c.op !== op) { card.style.opacity = op; c.op = op; } } } // ── Self-terminating RAF loop ── function loop() { update(); const diff = Math.abs(targetProgress - progress); if (diff > 0.01 || isDown) { rafId = requestAnimationFrame(loop); } else { progress = targetProgress; update(); isAnimating = false; rafId = null; } } function startLoop() { if (isAnimating) return; isAnimating = true; rafId = requestAnimationFrame(loop); } // ── Initial draw ── update(); // ── Event helpers ── function getX(e: MouseEvent | TouchEvent): number | undefined { return "touches" in e ? e.touches[0]?.clientX : (e as MouseEvent).clientX; } // ── Wheel ── function onWheel(e: WheelEvent) { const delta = e.deltaY * speedWheel; const next = targetProgress + delta; if ((next <= 0 && e.deltaY < 0) || (next >= 100 && e.deltaY > 0)) return; e.preventDefault(); targetProgress = Math.max(0, Math.min(100, next)); startLoop(); } // ── Drag ── function onDown(e: MouseEvent | TouchEvent) { isDown = true; const x = getX(e); if (x !== undefined) startX = x; // Keep loop alive during drag if (!isAnimating) { isAnimating = true; rafId = requestAnimationFrame(loop); } } function onMove(e: MouseEvent | TouchEvent) { if (!isDown) return; const x = getX(e); if (x === undefined) return; const diff = (x - startX) * speedDrag; targetProgress = Math.max(0, Math.min(100, targetProgress + diff)); startX = x; // No startLoop() — already running, loop checks isDown } function onUp() { if (!isDown) return; isDown = false; // Don't stop loop; it will self-terminate after settling } container.addEventListener("wheel", onWheel, { passive: false }); container.addEventListener("mousedown", onDown, { passive: true }); container.addEventListener("touchstart", onDown, { passive: true }); window.addEventListener("mousemove", onMove, { passive: true }); window.addEventListener("mouseup", onUp, { passive: true }); window.addEventListener("touchmove", onMove, { passive: true }); window.addEventListener("touchend", onUp, { passive: true }); return () => { container.removeEventListener("wheel", onWheel); container.removeEventListener("mousedown", onDown); container.removeEventListener("touchstart", onDown); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); window.removeEventListener("touchmove", onMove); window.removeEventListener("touchend", onUp); if (rafId) cancelAnimationFrame(rafId); cards.forEach(c => c.remove()); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [items, speedWheel, speedDrag]); return (
); }; export default ThreeDSlider; ``` -------------------------------------------------- ### COMPONENT: angled-slider Category: 3D Elements Description: A 3D perspective infinite slider with reflection effects and hover interactions. URL: https://lightswind.com/components/angled-slider Import: import { AngledSlider } from "@/components/lightswind/angled-slider" Registry URL: https://lightswind.com/r/angled-slider.json Install Command: npx lightswind@latest add angled-slider Usage: ```tsx import { AngledSlider } from "@/components/lightswind/angled-slider"; export function AngledSliderDemo() { const images = [ { id: 1, url: "https://images.unsplash.com/photo-1554080353-a576cf803bda?auto=format&fit=crop&w=1000&q=80", title: "Mountain View", }, { id: 2, url: "https://images.unsplash.com/photo-1505144808419-1957a94ca61e?auto=format&fit=crop&w=1000&q=80", title: "Ocean Breeze", }, { id: 3, url: "https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?auto=format&fit=crop&w=1000&q=80", title: "Forest Mist", }, { id: 4, url: "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?auto=format&fit=crop&w=1000&q=80", title: "Canyon Echo", }, { id: 5, url: "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?auto=format&fit=crop&w=1000&q=80", title: "Canyon Echo", }, { id: 6, url: "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?auto=format&fit=crop&w=1000&q=80", title: "Canyon Echo", }, ]; return ; } ``` Source Code: ```tsx "use client"; import React, { useRef, useEffect, useState } from "react"; import { motion, useMotionValue, animate, Variants } from "framer-motion"; import { cn } from "@/components/lib/utils"; import Image from "next/image"; interface AngledSliderProps { /** * Array of image objects or URLs */ items: { id: string | number; url: string; alt?: string; title?: string; }[]; /** * Speed of auto-scroll (seconds for full loop). Higher is slower. * @default 20 */ speed?: number; /** * Direction of scroll * @default "left" */ direction?: "left" | "right"; /** * Height of the slider container * @default "400px" */ containerHeight?: string; /** * Width of each card * @default "300px" */ cardWidth?: string; /** * Gap between cards * @default "40px" */ gap?: string; /** * Angle of the 3D skew/rotation * @default 20 */ angle?: number; /** * Scale on hover * @default 1.05 */ hoverScale?: number; className?: string; } const cardVariants: Variants = { offHover: (angle: number) => ({ rotateY: angle, z: 60, // Ensure card is in front of container plane (which blocks -Z events) opacity: 0.9, scale: 1, zIndex: 30, // Higher than potential overlays transition: { type: "spring", mass: 3, stiffness: 400, damping: 50 } }), onHover: (hoverScale: number) => ({ rotateY: 0, z: 120, // Pop out further opacity: 1, scale: hoverScale, zIndex: 50, transition: { type: "spring", mass: 3, stiffness: 400, damping: 50 } }) }; const AngledCard = ({ item, angle, hoverScale, cardWidth }: { item: any; angle: number; hoverScale: number; cardWidth: string; }) => { const [isHovered, setIsHovered] = useState(false); return ( setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {/* The Image Card */}
{item.alt {/* Optional Overlay/Title */} {item.title && (

{item.title}

)}
); }; export const AngledSlider = ({ items, speed = 40, direction = "left", containerHeight = "400px", cardWidth = "300px", gap = "40px", angle = 20, hoverScale = 1.05, className, }: AngledSliderProps) => { const [width, setWidth] = useState(0); const containerRef = useRef(null); const x = useMotionValue(0); const [isHovered, setIsHovered] = useState(false); // Duplicate items for infinite loop effect // We need enough duplicates to fill the screen + buffer const duplicatedItems = [...items, ...items, ...items]; useEffect(() => { const calculateWidth = () => { // Fallback to prop-based calculation if ref is not quite ready or layout is shifting // This is generally safer for known fixed-width items const numWidth = parseInt(cardWidth?.toString().replace("px", "") || "300"); const numGap = parseInt(gap?.toString().replace("px", "") || "40"); if (!isNaN(numWidth) && !isNaN(numGap)) { const calculatedWidth = (numWidth + numGap) * items.length; setWidth(calculatedWidth); } else if (containerRef.current) { const scrollWidth = containerRef.current.scrollWidth; setWidth(scrollWidth / 3); } }; calculateWidth(); window.addEventListener('resize', calculateWidth); return () => window.removeEventListener('resize', calculateWidth); }, [items, cardWidth, gap]); useEffect(() => { if (width <= 0) return; const startX = direction === "left" ? 0 : -width; const endX = direction === "left" ? -width : 0; if (isHovered) return; const runAnimation = () => { const currentX = x.get(); const totalDist = width; const dist = Math.abs(endX - currentX); const duration = speed * (dist / totalDist); const controls = animate(x, endX, { duration: duration, ease: "linear", onComplete: () => { x.set(startX); runAnimation(); } }); return controls; }; const animation = runAnimation(); return () => { animation.stop(); }; }, [width, speed, direction, isHovered, x]); return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {duplicatedItems.map((item, index) => ( ))}
); }; ``` -------------------------------------------------- ### COMPONENT: beam-circle Category: 3D Elements Description: A dynamic, orbit-based visual component where icons revolve around a customizable center element. It simulates rotating energy beams or planetary motion with full Framer Motion animation control. Perfect for loaders, dashboards, hero animations, or brand identity effects. URL: https://lightswind.com/components/beam-circle Import: import { BeamCircle } from "@/components/lightswind/beam-circle"; Registry URL: https://lightswind.com/r/beam-circle.json Install Command: npx lightswind@latest add beam-circle Usage: ```tsx // 1. Default beam circle with animated orbits // 2. Custom size, center icon, orbit colors, and line thickness } orbits={[ { id: 1, radiusFactor: 0.25, speed: 10, icon: , iconSize: 24, orbitColor: "rgba(0, 150, 255, 0.3)", orbitThickness: 3, }, { id: 2, radiusFactor: 0.55, speed: 14, icon: , iconSize: 30, orbitThickness: 2, }, ]} /> ``` Source Code: ```tsx "use client"; import React, { useMemo } from "react"; import { motion, Transition } from "framer-motion"; import { Sun, Cloud, MessageSquare, Briefcase, Zap } from "lucide-react"; // --- Types --- type OrbitConfig = { id: number; radiusFactor: number; speed: number; // seconds per rotation icon: React.ReactNode; iconSize: number; orbitColor?: string; orbitThickness?: number; }; type BeamCircleProps = { size?: number; orbits?: OrbitConfig[]; centerIcon?: React.ReactNode; }; // --- Default Orbits --- const defaultOrbits: OrbitConfig[] = [ { id: 1, radiusFactor: 0.15, speed: 7, icon: , iconSize: 20, orbitColor: "rgba(255, 193, 7, 0.4)", orbitThickness: 1.5, }, { id: 2, radiusFactor: 0.35, speed: 12, icon: , iconSize: 24, orbitThickness: 1.5, }, { id: 3, radiusFactor: 0.55, speed: 9, icon: , iconSize: 28, orbitColor: "rgba(76, 175, 80, 0.4)", orbitThickness: 2, }, { id: 4, radiusFactor: 0.75, speed: 15, icon: , iconSize: 32, orbitThickness: 1, }, ]; // --- Component --- const BeamCircle: React.FC = ({ size = 300, orbits: customOrbits, centerIcon, }) => { const orbitsData = useMemo(() => customOrbits || defaultOrbits, [customOrbits]); const halfSize = size / 2; // --- Define linear easing manually --- const linearEase = (t: number) => t; const rotationTransition = (duration: number): Transition => ({ repeat: Infinity, duration, ease: linearEase, // ✅ Works properly now }); // --- Center Icon --- const CenterIcon = useMemo( () => ( {centerIcon ? ( centerIcon ) : ( )} ), [halfSize, centerIcon] ); return (
{orbitsData.map((orbit) => { const orbitDiameter = size * orbit.radiusFactor; const orbitRadius = orbitDiameter / 2; const containerSize = size; return ( {/* Orbit Line */}
{/* Rotating Container */} {/* Traveling Icon */}
{React.isValidElement(orbit.icon) ? ( React.cloneElement(orbit.icon as any, { size: orbit.iconSize * 0.6, }) ) : ( orbit.icon )}
); })} {/* Central Icon */}
{CenterIcon}
); }; export default BeamCircle; ``` -------------------------------------------------- ### COMPONENT: 3d-beam-circle Category: 3D Elements Description: A stunning 3D perspective orbital ring component where glowing boxes travel along concentric elliptical orbits. Each orbit is independently configurable with custom speed, direction (CW/CCW), color, glow, and traveler box size. Built with pure SVG + rAF — no external 3D library required. Perfect for loaders, hero sections, tech dashboards, or orbit/network visualizations. URL: https://lightswind.com/components/3d-beam-circle Import: import { ThreeDBeamCircle } from "@/components/lightswind/3d-beam-circle"; Registry URL: https://lightswind.com/r/3d-beam-circle.json Install Command: npx lightswind@latest add 3d-beam-circle Usage: ```tsx // 1. Default 3D Beam Circle (4 orbits, auto-spaced, multicolor) // 2. Custom tilt, orbits, and center node } orbits={[ { id: 0, radiusFactor: 0.18, speed: 7, direction: "cw", orbitColor: "rgba(99,102,241,0.4)", boxColor: "#6366f1", glowColor: "#a5b4fc", boxWidth: 24, boxHeight: 14, delay: 0, children: , }, { id: 1, radiusFactor: 0.38, speed: 14, direction: "ccw", orbitColor: "rgba(168,85,247,0.35)", boxColor: "#a855f7", glowColor: "#c084fc", boxWidth: 30, boxHeight: 16, delay: 0.5, }, ]} /> ``` Source Code: ```tsx "use client"; import React, { useEffect, useRef, useState, useMemo } from "react"; import { cn } from "@/components/lib/utils"; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ export type BeamOrbitConfig = { /** Unique id per orbit */ id: number; /** Orbit radius as fraction of viewBox width (0–0.5). Default auto-spaced. */ radiusFactor?: number; /** Speed of travel along the arc in seconds. Default 10 */ speed?: number; /** Custom stroke color of the orbit arc line (overrides theme currentColor) */ orbitColor?: string; /** Stroke thickness of the orbit arc in px. Default uses global orbitThickness */ orbitThickness?: number; /** Opacity of this orbit's arc line (0–1). Default uses global arcOpacity */ orbitOpacity?: number; /** Badge background fill color */ boxColor?: string; /** Badge border/glow accent color */ glowColor?: string; /** Badge size (diameter) in px. Default 32 */ size?: number; /** Initial position offset along the arc (0–1 fraction). Default 0 */ delay?: number; /** * Direction of travel: * - "cw" — clockwise (left → right on semi-circle) * - "ccw" — counter-clockwise (right → left) * Default inherits global `orbitDirection` */ direction?: "cw" | "ccw"; /** Optional element/flag/icon rendered inside the round badge */ children?: React.ReactNode; }; export type ThreeDBeamCircleProps = { /* ── Layout ─────────────────────────────────────────────── */ /** CSS width of the root container. Default "100%" */ width?: string | number; /** CSS height of the root container. Default "100%" */ height?: string | number; /** SVG internal viewBox width. Default 800 */ viewBoxWidth?: number; /** SVG internal viewBox height. Default 440 */ viewBoxHeight?: number; /* ── Mode ───────────────────────────────────────────────── */ /** * "semi-circle" — half-dome anchored at the bottom (default) * "full-circle" — full orbital rings centered in view */ mode?: "semi-circle" | "full-circle"; /* ── Orbits ─────────────────────────────────────────────── */ /** Array of orbit ring configurations */ orbits?: BeamOrbitConfig[]; /** * Global default direction for all badges unless overridden per-orbit. * "cw" = clockwise (default), "ccw" = counter-clockwise */ orbitDirection?: "cw" | "ccw"; /* ── Arc Appearance ─────────────────────────────────────── */ /** Global default stroke thickness for all orbit arcs. Default 1.5 */ orbitThickness?: number; /** * Global arc line opacity (0–1). Each orbit can override via orbitOpacity. * Default 0.75 */ arcOpacity?: number; /** * Intensity of the gradient fade effect on arc edges (0–1). * 0 = solid line, 1 = maximum fade. Default 0.75 */ arcGlowIntensity?: number; /** Hide all arc lines and only show the traveling badges. Default false */ showArcs?: boolean; /* ── Badge Appearance ───────────────────────────────────── */ /** * Border width (px) for all traveler badges. * Default 1 */ badgeBorderWidth?: number; /** * Border color for all badges when no per-orbit glowColor is set. * Accepts any CSS color string. Default "currentColor" (theme foreground) */ badgeBorderColor?: string; /** * Badge drop shadow size. "none" | "sm" | "md" | "lg" | "glow" * Default "md" */ badgeShadow?: "none" | "sm" | "md" | "lg" | "glow"; /** * Badge border radius override — useful for square/pill badges. * Default "9999px" (fully round) */ badgeBorderRadius?: string; /* ── Center Node ─────────────────────────────────────────── */ /** Content rendered at the center origin point */ centerContent?: React.ReactNode; /** * Diameter of the center node foreignObject in px. Default 52 */ centerNodeSize?: number; /** Show subtle ambient glow at center origin. Default true */ centerGlow?: boolean; /* ── Animation ──────────────────────────────────────────── */ /** Pause all badge animations. Default false */ animationPaused?: boolean; /** * Global speed multiplier applied to all orbits. * 0.5 = half speed, 2 = double speed. Default 1 */ speedMultiplier?: number; /* ── Misc ───────────────────────────────────────────────── */ /** Extra class name for the root wrapper element */ className?: string; }; /* ------------------------------------------------------------------ */ /* Default Orbits */ /* ------------------------------------------------------------------ */ const DEFAULT_SEMI_ORBITS: BeamOrbitConfig[] = [ { id: 0, radiusFactor: 0.15, speed: 8, size: 28, delay: 0.1 }, { id: 1, radiusFactor: 0.26, speed: 12, size: 32, delay: 0.4 }, { id: 2, radiusFactor: 0.37, speed: 16, size: 34, delay: 0.7 }, { id: 3, radiusFactor: 0.46, speed: 22, size: 36, delay: 0.88 }, ]; /* ------------------------------------------------------------------ */ /* Shadow utility */ /* ------------------------------------------------------------------ */ function resolveShadow( shadow: ThreeDBeamCircleProps["badgeShadow"], glowColor?: string ): string { if (glowColor) return `0 0 16px ${glowColor}66, 0 2px 8px rgba(0,0,0,0.15)`; switch (shadow) { case "none": return "none"; case "sm": return "0 1px 4px rgba(0,0,0,0.10)"; case "lg": return "0 4px 20px rgba(0,0,0,0.22)"; case "glow": return "0 0 24px rgba(0,0,0,0.30), 0 2px 12px rgba(0,0,0,0.18)"; default: return "0 2px 8px rgba(0,0,0,0.12)"; // "md" } } /* ------------------------------------------------------------------ */ /* ArcTravelerBadge */ /* ------------------------------------------------------------------ */ function ArcTravelerBadge({ cx, cy, radius, speed = 10, delay = 0, badgeSize = 32, boxColor, glowColor, isFullCircle = false, direction = "cw", paused = false, speedMultiplier = 1, borderWidth = 1, borderColor, shadow = "md", borderRadius = "9999px", children, }: { cx: number; cy: number; radius: number; speed?: number; delay?: number; badgeSize?: number; boxColor?: string; glowColor?: string; isFullCircle?: boolean; direction?: "cw" | "ccw"; paused?: boolean; speedMultiplier?: number; borderWidth?: number; borderColor?: string; shadow?: ThreeDBeamCircleProps["badgeShadow"]; borderRadius?: string; children?: React.ReactNode; }) { const frameRef = useRef(undefined); const lastRef = useRef(0); const [pos, setPos] = useState({ x: cx - radius, y: cy }); const tAccRef = useRef(delay * speed); useEffect(() => { const tick = (ts: number) => { const dt = lastRef.current ? (ts - lastRef.current) / 1000 : 0; lastRef.current = ts; if (!paused) { tAccRef.current += dt * speedMultiplier; } const t = tAccRef.current; let x: number, y: number; if (isFullCircle) { const angle = (t / speed) * 2 * Math.PI * (direction === "ccw" ? -1 : 1); x = cx + radius * Math.cos(angle); y = cy + radius * Math.sin(angle); } else { const progress = (Math.sin((t / speed) * Math.PI) + 1) / 2; const rawAngle = Math.PI - progress * Math.PI; const angle = direction === "ccw" ? Math.PI - rawAngle : rawAngle; x = cx + radius * Math.cos(angle); y = cy - radius * Math.sin(angle); } setPos({ x, y }); frameRef.current = requestAnimationFrame(tick); }; frameRef.current = requestAnimationFrame(tick); return () => { if (frameRef.current) cancelAnimationFrame(frameRef.current); }; }, [cx, cy, radius, speed, delay, isFullCircle, direction, paused, speedMultiplier]); const resolvedBorder = glowColor ? `${glowColor}99` : (borderColor ?? "currentColor"); return (
{children}
); } /* ------------------------------------------------------------------ */ /* Main Component */ /* ------------------------------------------------------------------ */ export const ThreeDBeamCircle: React.FC = ({ width = "100%", height = "100%", viewBoxWidth = 800, viewBoxHeight = 440, mode = "semi-circle", orbits: customOrbits, orbitDirection = "cw", orbitThickness = 1.5, arcOpacity = 0.75, arcGlowIntensity = 0.75, showArcs = true, badgeBorderWidth = 1, badgeBorderColor, badgeShadow = "md", badgeBorderRadius = "9999px", centerContent, centerNodeSize = 52, centerGlow = true, animationPaused = false, speedMultiplier = 1, className, }) => { const orbits = useMemo(() => customOrbits ?? DEFAULT_SEMI_ORBITS, [customOrbits]); const cx = viewBoxWidth / 2; const cy = mode === "semi-circle" ? viewBoxHeight - 12 : viewBoxHeight / 2; // Arc gradient mid-opacity derived from arcOpacity × arcGlowIntensity const midOpacity = arcOpacity * arcGlowIntensity; const edgeOpacity = arcOpacity * arcGlowIntensity * 0.6; return (
{orbits.map((orbit) => { const rf = orbit.radiusFactor ?? 0.3; const r = viewBoxWidth * rf; const gradId = `arcGrad_${orbit.id}`; const x0 = cx - r; const x1 = cx + r; const oMid = orbit.orbitOpacity !== undefined ? orbit.orbitOpacity * arcGlowIntensity : midOpacity; const oEdge = orbit.orbitOpacity !== undefined ? orbit.orbitOpacity * arcGlowIntensity * 0.6 : edgeOpacity; return ( ); })} {/* Orbit Arc Lines */} {showArcs && orbits.map((orbit) => { const rf = orbit.radiusFactor ?? 0.3; const r = viewBoxWidth * rf; const thickness = orbit.orbitThickness ?? orbitThickness; const gradId = `arcGrad_${orbit.id}`; return ( {mode === "semi-circle" ? ( ) : ( )} ); })} {/* Traveling Badges */} {orbits.map((orbit) => { const rf = orbit.radiusFactor ?? 0.3; const r = viewBoxWidth * rf; return ( {orbit.children} ); })} {/* Center Origin Node */} {centerContent && (
{centerContent}
)}
); }; export default ThreeDBeamCircle; ``` -------------------------------------------------- ### COMPONENT: chain-carousel Category: 3D Elements Description: A horizontally scrolling carousel component for displaying blockchain chains, tokens, or custom items. It features auto-scroll with smooth Framer Motion animations, center highlighting, left/right mirrored displays, and an interactive search dropdown for selecting and focusing on specific items. Perfect for dashboards, explorers, and interactive listings. URL: https://lightswind.com/components/chain-carousel Import: import ChainCarousel from "@/components/lightswind/chain-carousel"; Registry URL: https://lightswind.com/r/chain-carousel.json Install Command: npx lightswind@latest add chain-carousel Usage: ```tsx // 1. Default carousel with auto-scroll // 2. Custom visible items, scroll speed, and selection callback console.log("Selected:", id, name)} /> ``` Source Code: ```tsx import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { motion, useInView } from 'framer-motion'; import { LucideIcon, Search, DeleteIcon } from 'lucide-react'; // NOTE: Placeholder for your custom Input component const Input = (props: React.InputHTMLAttributes) => ( ); // --- Core Data Interface --- export interface ChainItem { id: string | number; // Unique ID name: string; icon: LucideIcon; /** A secondary string line for the item, e.g., a short description or a value. */ details?: string; logo?: string; // Optional image URL } // --- Internal Animated Type --- /** The specific type returned by getVisibleItems, extending the base ChainItem. */ type AnimatedChainItem = ChainItem & { distanceFromCenter: number; originalIndex: number; }; // --- Component Props Interfaces --- interface CarouselItemProps { chain: AnimatedChainItem; side: 'left' | 'right'; } interface ChainCarouselProps { /** The list of items to display in the carousel. (REQUIRED) */ items: ChainItem[]; /** The speed of the auto-scroll rotation in milliseconds. */ scrollSpeedMs?: number; /** The number of carousel items visible at once (must be an odd number). */ visibleItemCount?: number; /** Custom class for the main container div. */ className?: string; /** Function to call when a chain is selected from the search dropdown. */ onChainSelect?: (chainId: ChainItem['id'], chainName: string) => void; } // --- Helper Components --- /** A single item card for the carousel. */ const CarouselItemCard: React.FC = ({ chain, side }) => { const { distanceFromCenter, id, name, details, logo, icon: FallbackIcon } = chain; const distance = Math.abs(distanceFromCenter); // Visual effects based on distance from the center (0) const opacity = 1 - distance / 4; const scale = 1 - distance * 0.1; const yOffset = distanceFromCenter * 90; const xOffset = side === 'left' ? -distance * 50 : distance * 50; const IconOrLogo = (
{logo ? ( {`${name} ) : ( )}
); return ( {IconOrLogo}
{/* FIX: Added whitespace-nowrap to prevent the name from wrapping. */} {name} {/* Display generic details/description */} {details}
); }; // --- Main Component --- const ChainCarousel: React.FC = ({ items, scrollSpeedMs = 1500, visibleItemCount = 9, className = '', onChainSelect, }) => { const [currentIndex, setCurrentIndex] = useState(0); const [isPaused, setIsPaused] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [showDropdown, setShowDropdown] = useState(false); // References for Framer Motion scroll-based animation const rightSectionRef = useRef(null); const isInView = useInView(rightSectionRef, { margin: '-100px 0px -100px 0px' }); const totalItems = items.length; // 1. Auto-scroll effect useEffect(() => { if (isPaused || totalItems === 0) return; const interval = setInterval(() => { setCurrentIndex((prev) => (prev + 1) % totalItems); }, scrollSpeedMs); return () => clearInterval(interval); }, [isPaused, totalItems, scrollSpeedMs]); // 2. Scroll listener to pause carousel on page scroll useEffect(() => { let timeoutId: NodeJS.Timeout; const handleScroll = () => { setIsPaused(true); clearTimeout(timeoutId); timeoutId = setTimeout(() => { setIsPaused(false); }, 500); // Resume auto-scroll after 500ms of no scrolling }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => { window.removeEventListener('scroll', handleScroll); clearTimeout(timeoutId); }; }, []); // Memoized function for carousel items const getVisibleItems = useCallback( (): AnimatedChainItem[] => { // Explicitly define return type const visibleItems: AnimatedChainItem[] = []; if (totalItems === 0) return []; // Ensure visibleItemCount is an odd number for a clear center item const itemsToShow = visibleItemCount % 2 === 0 ? visibleItemCount + 1 : visibleItemCount; const half = Math.floor(itemsToShow / 2); for (let i = -half; i <= half; i++) { let index = currentIndex + i; if (index < 0) index += totalItems; if (index >= totalItems) index -= totalItems; visibleItems.push({ ...items[index], originalIndex: index, distanceFromCenter: i, }); } return visibleItems; }, [currentIndex, items, totalItems, visibleItemCount] ); // Filtered list for search dropdown const filteredItems = useMemo(() => { return items.filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase()) ); }, [items, searchTerm]); // Handler for selecting an item from the dropdown const handleSelectChain = (id: ChainItem['id'], name: string) => { const index = items.findIndex((c) => c.id === id); if (index !== -1) { setCurrentIndex(index); // Jump to the selected item setIsPaused(true); // Pause to highlight the selection if (onChainSelect) { } } setSearchTerm(name); // Set search term to the selected item's name setShowDropdown(false); }; // The current item displayed in the center const currentItem = items[currentIndex]; // --- JSX Render --- return (
{/* Left Section - Chain Carousel (Hidden on smaller screens) */} !searchTerm && setIsPaused(true)} onMouseLeave={() => !searchTerm && setIsPaused(false)} initial={{ x: '-100%', opacity: 0 }} animate={isInView ? { x: 0, opacity: 1 } : {}} transition={{ type: 'spring', stiffness: 80, damping: 20, duration: 0.8 }} > {/* Fading overlay to mask items */}
{getVisibleItems().map((chain) => ( ))}
{/* Middle Section - Text and Search Input */}
{/* Currently Selected Item Display */} {currentItem && (
{currentItem.logo ? ( {`${currentItem.name} ) : ( )}

{currentItem.name}

{currentItem.details || 'View Details'}

)} {/* Search Bar */}
{ const val = e.target.value; setSearchTerm(val); setShowDropdown(val.length > 0); if (val === '') setIsPaused(false); }} onFocus={() => { if (searchTerm.length > 0) setShowDropdown(true); setIsPaused(true); }} onBlur={() => { // Wait briefly before hiding to allow click on dropdown item setTimeout(() => setShowDropdown(false), 200); }} className="flex-grow outline-none text-foreground bg-background px-4 !placeholder-gray-800 text-lg rounded-full border-gray-500 pr-10 pl-10 py-2 cursor-pointer border" /> {searchTerm && ( )}
{/* Dropdown for search results */} {showDropdown && filteredItems.length > 0 && (
{filteredItems.slice(0, 10).map((chain) => (
{ e.preventDefault(); handleSelectChain(chain.id, chain.name); }} className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-gray-100/10 transition-colors rounded-lg m-2" > {chain.logo ? ( {`${chain.name} ) : ( )} {chain.name} {chain.details}
))}
)}
{/* Right Section - Chain Carousel */} !searchTerm && setIsPaused(true)} onMouseLeave={() => !searchTerm && setIsPaused(false)} initial={{ x: '100%', opacity: 0 }} animate={isInView ? { x: 0, opacity: 1 } : {}} transition={{ type: 'spring', stiffness: 80, damping: 20, duration: 0.8 }} > {/* Fading overlay to mask items */}
{getVisibleItems().map((chain) => ( ))}
); }; export default ChainCarousel; ``` -------------------------------------------------- ### COMPONENT: infinite-drift Category: 3D Elements Description: A high-performance Three.js based image marquee with independent horizontal bands, custom vertex shaders for curvature effects, and smooth inertial scrolling. Ideal for hero sections or premium galleries. URL: https://lightswind.com/components/infinite-drift Import: import { InfiniteDrift } from "@/components/lightswind/infinite-drift" Registry URL: https://lightswind.com/r/infinite-drift.json Install Command: npx lightswind@latest add infinite-drift Usage: ```tsx import { InfiniteDrift } from "@/components/lightswind/infinite-drift"; const bands = [ { speed: 1.0, rotation: 7, curveAmount: 40.0, images: ["/img1.jpg", "/img2.jpg", "/img3.jpg"], }, { speed: 1.3, offsetY: -100, images: ["/img4.jpg", "/img5.jpg", "/img6.jpg"], } ]; export function Gallery() { return ( ); } ``` Source Code: ```tsx "use client"; import React, { useRef, useEffect } from "react"; import { cn } from "../lib/utils"; export interface InfiniteDriftBand { images: string[]; speed?: number; rotation?: number; offsetY?: number; curveAmount?: number; curveDirection?: 1 | -1; rotationType?: "fromLeft" | "fromCenter"; } export interface InfiniteDriftProps { bands?: InfiniteDriftBand[]; height?: string | number; gap?: number; imageHeight?: number; bandHeight?: number; maxImageWidth?: number; inertia?: number; preserveOriginalRatios?: boolean; className?: string; children?: React.ReactNode; } const DEFAULT_BANDS: InfiniteDriftBand[] = [ { offsetY: -220, speed: 1.0, rotation: 7, rotationType: "fromLeft", curveAmount: 40.0, curveDirection: 1, images: ["https://images.unsplash.com/photo-1550684848-fac1c5b4e853?w=400","https://images.unsplash.com/photo-1550684399-3f0f745771d1?w=400","https://images.unsplash.com/photo-1550684847-75bdda21cc95?w=400","https://images.unsplash.com/photo-1563089145-599997674d42?w=400"] }, { offsetY: -110, speed: 1.3, rotation: 7, rotationType: "fromCenter", curveAmount: 35.0, curveDirection: 1, images: ["https://images.unsplash.com/photo-1557683316-973673baf926?w=400","https://images.unsplash.com/photo-1557683311-eac922347aa1?w=400","https://images.unsplash.com/photo-1557683325-3ba8f0df79de?w=400","https://images.unsplash.com/photo-1561070791-2526d30994b5?w=400"] }, { offsetY: 0, speed: 0.7, rotation: 7, curveAmount: 40.0, curveDirection: 1, images: ["https://images.unsplash.com/photo-1511447333849-2b89ae13b002?w=400","https://images.unsplash.com/photo-1536431311719-398b6704d4cc?w=400","https://images.unsplash.com/photo-1543966888-7c1dc482a810?w=400","https://images.unsplash.com/photo-1561070791-36c11767b26a?w=400"] }, { offsetY: 110, speed: 1.2, rotation: 7, curveAmount: 40.0, curveDirection: 1, images: ["https://images.unsplash.com/photo-1493246507139-91e8bef99c02?w=400","https://images.unsplash.com/photo-1477346611705-65d1883cee1e?w=400","https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=400","https://images.unsplash.com/photo-1561070791-0626bcd0516e?w=400"] }, { offsetY: 220, speed: 0.9, rotation: 7, curveAmount: 35.0, curveDirection: 1, images: ["https://images.unsplash.com/photo-1534067783941-51c9c23ecefd?w=400","https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400","https://images.unsplash.com/photo-1550684848-fac1c5b4e853?w=400","https://images.unsplash.com/photo-1557683316-973673baf926?w=400"] }, ]; // Replicate GLSL mod() – always positive remainder const glslMod = (a: number, b: number) => ((a % b) + b) % b; // Replicate GLSL rotate2d() – rotate point (px,py) around centre (cx,cy) const rotate2dPt = (px: number, py: number, cx: number, cy: number, angle: number): [number, number] => { const dx = px - cx, dy = py - cy, c = Math.cos(angle), s = Math.sin(angle); return [cx + c * dx - s * dy, cy + s * dx + c * dy]; }; interface BandState { config: InfiniteDriftBand; stripCanvas: HTMLCanvasElement; sequenceWidth: number; ready: boolean; } export const InfiniteDrift: React.FC = ({ bands = DEFAULT_BANDS, height = 600, gap = 20, imageHeight = 100, bandHeight = 120, maxImageWidth = 300, inertia = 0.92, preserveOriginalRatios = true, className, children, }) => { const containerRef = useRef(null); const canvasRef = useRef(null); const scrollState = useRef({ scrollY: 0, targetScrollY: 0, scrollVelocity: 0, isDragging: false, lastMouseY: 0, }); useEffect(() => { if (!containerRef.current || !canvasRef.current) return; const container = containerRef.current; const canvas = canvasRef.current; const dpr = Math.min(window.devicePixelRatio, 2); const resize = () => { canvas.width = container.clientWidth * dpr; canvas.height = container.clientHeight * dpr; // Re-apply DPR scale after every resize – resizing resets the transform const c = canvas.getContext("2d"); if (c) c.scale(dpr, dpr); }; resize(); const ctx = canvas.getContext("2d"); if (!ctx) return; // guard: stale WebGL context from HMR or SSR // ---- Build one offscreen strip per band (3-clone texture atlas) ---- const bandStates: BandState[] = bands.map((config) => ({ config, stripCanvas: document.createElement("canvas"), sequenceWidth: 0, ready: false, })); const loadBand = async (bs: BandState) => { const { config } = bs; const imgs = await Promise.all(config.images.map((url) => new Promise((resolve) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => resolve(img); img.onerror = () => { const fb = document.createElement("canvas"); fb.width = 400; fb.height = 300; const fc = fb.getContext("2d")!; fc.fillStyle = `hsl(${Math.random() * 360},70%,60%)`; fc.fillRect(0, 0, 400, 300); resolve(fb); }; img.src = url; }) )); let seqW = 0; const infos = imgs.map((src) => { const nw = (src as HTMLImageElement).naturalWidth || (src as HTMLCanvasElement).width; const nh = (src as HTMLImageElement).naturalHeight || (src as HTMLCanvasElement).height; const ratio = nw / nh || 1.5; let w: number, h: number; if (preserveOriginalRatios) { h = imageHeight; w = Math.round(h * ratio); if (w > maxImageWidth) { w = maxImageWidth; h = Math.round(w / ratio); } } else { h = imageHeight; w = Math.round(h * 1.5); } seqW += w + gap; return { src, w, h }; }); seqW -= gap; // remove trailing gap const CLONES = 3; const strip = bs.stripCanvas; strip.width = seqW * CLONES; strip.height = bandHeight; const sc = strip.getContext("2d")!; let cx2 = 0; for (let c = 0; c < CLONES; c++) { for (const info of infos) { sc.drawImage(info.src, cx2, (bandHeight - info.h) / 2, info.w, info.h); cx2 += info.w + gap; } } bs.sequenceWidth = seqW; bs.ready = true; }; void Promise.all(bandStates.map(loadBand)); // ---- Render one band – exact GLSL shader math in Canvas 2D ---- // Shader replication: // curveOffset(x) = (0.5 - 4*(nx-0.5)^2) * curveAmount * curveDirection // rotation: rotate point around pivot before texture lookup // wrapping: glslMod(srcX + scrollPos, sequenceWidth) const drawBand = (bs: BandState, W: number, H: number, scrollY: number) => { if (!bs.ready) return; const { config, stripCanvas, sequenceWidth } = bs; const rotRad = ((config.rotation || 0) * Math.PI) / 180; const hasRot = Math.abs(rotRad) > 0.0001; const curveAmount = config.curveAmount ?? 0; const curveDir = config.curveDirection ?? 1; const speed = config.speed ?? 1.0; const offsetY = config.offsetY ?? 0; const fromLeft = config.rotationType === "fromLeft"; // Mirrors shader: bandTopBase = (uResolution.y - uBandHeight) * 0.5 + uOffsetY const bandTopBase = (H - bandHeight) * 0.5 + offsetY; const bandCenterY = bandTopBase + bandHeight * 0.5; const pivotX = fromLeft ? 0 : W * 0.5; const pivotY = bandCenterY; const scrollPos = scrollY * speed; ctx.save(); // Clip to band region (with curvature padding + rotation padding) ctx.beginPath(); if (hasRot) { const pad = curveAmount + 4; const corners: [number, number][] = [ [0, bandTopBase - pad], [W, bandTopBase - pad], [W, bandTopBase + bandHeight + pad], [0, bandTopBase + bandHeight + pad], ]; corners.forEach(([cx_, cy_], i) => { const [rx, ry] = rotate2dPt(cx_, cy_, pivotX, pivotY, -rotRad); if (i === 0) ctx.moveTo(rx, ry); else ctx.lineTo(rx, ry); }); ctx.closePath(); } else { const pad = curveAmount + 4; ctx.rect(0, bandTopBase - pad, W, bandHeight + pad * 2); } ctx.clip(); // Column-slice loop – replicates per-pixel shader logic for (let x = 0; x < W; x++) { // 1. Parabolic curvature: curveOffset = (0.5 - 4*(nx-0.5)^2) * amount * dir const nx = x / W; const curveFactor = 4.0 * (nx - 0.5) * (nx - 0.5); const curveOffset = (0.5 - curveFactor) * curveAmount * curveDir; const bandTop = bandTopBase + curveOffset; // 2. Rotation inverse – find source x in strip texture let srcX = x; if (hasRot) { const [rx] = rotate2dPt(x, bandCenterY, pivotX, pivotY, rotRad); srcX = rx; } // 3. Infinite wrapping via GLSL mod const wrappedX = glslMod(srcX + scrollPos, sequenceWidth); const texSrcX = wrappedX + sequenceWidth; // use 2nd tile (of 3) for stability if (texSrcX < 0 || texSrcX >= stripCanvas.width) continue; // 4. Draw 1-px column from strip onto output at curved position ctx.drawImage(stripCanvas, texSrcX, 0, 1, bandHeight, x, bandTop, 1, bandHeight); } ctx.restore(); }; // ---- Animation loop ---- let animId: number; const animate = () => { animId = requestAnimationFrame(animate); const W = container.clientWidth, H = container.clientHeight; const state = scrollState.current; if (!state.isDragging) { state.targetScrollY += state.scrollVelocity; state.scrollVelocity *= inertia; if (Math.abs(state.scrollVelocity) < 0.1) state.scrollVelocity = 0; } state.scrollY += (state.targetScrollY - state.scrollY) * 0.1; ctx.clearRect(0, 0, W, H); for (const bs of bandStates) drawBand(bs, W, H, state.scrollY); }; animate(); // ---- Event handlers – identical to original ---- const handleWheel = (e: WheelEvent) => { e.preventDefault(); scrollState.current.targetScrollY += e.deltaY; scrollState.current.scrollVelocity = e.deltaY * 0.15; }; let lastScrollTop = window.scrollY; const handleGlobalScroll = () => { const c = window.scrollY, d = c - lastScrollTop; lastScrollTop = c; scrollState.current.targetScrollY += d * 2.5; scrollState.current.scrollVelocity = d * 0.3; }; const handleMouseDown = (e: MouseEvent) => { scrollState.current.isDragging = true; scrollState.current.lastMouseY = e.clientY; scrollState.current.scrollVelocity = 0; }; const handleMouseMove = (e: MouseEvent) => { if (!scrollState.current.isDragging) return; const d = e.clientY - scrollState.current.lastMouseY; scrollState.current.targetScrollY += d * 2.0; scrollState.current.lastMouseY = e.clientY; scrollState.current.scrollVelocity = d * 0.25; }; const handleMouseUp = () => { scrollState.current.isDragging = false; }; const handleResize = () => { resize(); }; // scale is re-applied inside resize() const handleTouchStart = (e: TouchEvent) => { scrollState.current.lastMouseY = e.touches[0].clientY; scrollState.current.isDragging = true; }; const handleTouchMove = (e: TouchEvent) => { if (!scrollState.current.isDragging) return; const d = e.touches[0].clientY - scrollState.current.lastMouseY; scrollState.current.targetScrollY += d * 2.5; scrollState.current.lastMouseY = e.touches[0].clientY; scrollState.current.scrollVelocity = d * 0.3; }; container.addEventListener("wheel", handleWheel, { passive: false }); container.addEventListener("mousedown", handleMouseDown); window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); window.addEventListener("resize", handleResize); window.addEventListener("scroll", handleGlobalScroll, { passive: true }); container.addEventListener("touchstart", handleTouchStart, { passive: false }); container.addEventListener("touchmove", handleTouchMove, { passive: false }); container.addEventListener("touchend", handleMouseUp); return () => { cancelAnimationFrame(animId); container.removeEventListener("wheel", handleWheel); container.removeEventListener("mousedown", handleMouseDown); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener("resize", handleResize); window.removeEventListener("scroll", handleGlobalScroll); container.removeEventListener("touchstart", handleTouchStart); container.removeEventListener("touchmove", handleTouchMove); container.removeEventListener("touchend", handleMouseUp); }; }, [bands, gap, imageHeight, bandHeight, maxImageWidth, inertia, preserveOriginalRatios]); return (
{children}
); }; export default InfiniteDrift; ``` -------------------------------------------------- ### COMPONENT: hanging-id-card Category: 3D Elements Description: A physics-based ID card component that hangs from a rope at the top. Features realistic pendulum physics with spring return, drag interaction, and gravity simulation. Click or drag the card left/right and watch it swing with authentic momentum before settling back to center. URL: https://lightswind.com/components/hanging-id-card Import: import { HangingIdCard } from "@/components/lightswind/hanging-id-card" Registry URL: https://lightswind.com/r/hanging-id-card.json Install Command: npx lightswind@latest add hanging-id-card Usage: ```tsx import { HangingIdCard } from "@/components/lightswind/hanging-id-card"; export default function App() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useRef, useEffect, useCallback, useState } from "react"; import { cn } from "@/components/lib/utils"; // ─── Physics constants ──────────────────────────────────────────────────────── const SPRING_K = 0; // Real pendulum relies on gravity const DAMPING = 0.92; // Air resistance for smooth natural swing const GRAVITY = 3000; // Gravity scalar for snappy momentum const MASS = 1; interface CardPhysicsState { angle: number; // radians from vertical vel: number; // angular velocity rad/s } export interface HangingIdCardProps { children?: React.ReactNode; ropeLength?: number; ropeColor?: string; className?: string; name?: string; role?: string; badgeId?: string; accentColor?: string; } // ─── SVG Black Lanyard Rope & Metal Lock Clip ────────────────────────────────── const Lanyard = ({ length, color }: { length: number; color: string }) => { const clampY = length; const ringY = length + 10; const hookY = length + 18; return ( {/* Metal clamp & ring gradient */} {/* Hook gradient */} {/* Ribbon fabric texture shading */} {/* Main Lanyard Ribbon Strap */} {/* Strap fabric depth shading */} {/* Strap side stitch lines */} {/* Metallic Ribbon Crimp Clamp (Base of Strap) */} {/* Metallic Screws/Rivets on Clamp */} {/* Swivel Ring Loop */} {/* Swivel Joint */} {/* Metal Snap Hook / Lock Clip */} {/* Spring Clip Latch Lever */} ); }; // ─── Main Component ─────────────────────────────────────────────────────────── export const HangingIdCard = ({ children, ropeLength = 140, ropeColor = "#18181b", className, name = "John Doe", role = "Product Designer", badgeId = "ID-84920", accentColor = "#2563eb", }: HangingIdCardProps) => { const physRef = useRef({ angle: 0, vel: 0 }); const rafRef = useRef(null); const prevTimeRef = useRef(null); const prevAngleRef = useRef(0); const isDraggingRef= useRef(false); const [angle, setAngle] = useState(0); const [, setIsDragState] = useState(false); const dragStartX = useRef(0); const dragAngle0 = useRef(0); // ── Physics loop ──────────────────────────────────────────────────────────── const tick = useCallback((now: number) => { if (prevTimeRef.current === null) { prevTimeRef.current = now; } const dt = Math.min((now - prevTimeRef.current) / 1000, 0.05); // cap at 50ms prevTimeRef.current = now; const s = physRef.current; if (!isDraggingRef.current) { // Realistic pendulum: L is approximate center of mass const L = ropeLength + 100; const torque = -(GRAVITY / L) * Math.sin(s.angle) - (DAMPING / MASS) * s.vel - (SPRING_K / MASS) * s.angle; s.vel += torque * dt; s.angle += s.vel * dt; setAngle(s.angle); if (Math.abs(s.angle) > 0.001 || Math.abs(s.vel) > 0.001) { rafRef.current = requestAnimationFrame(tick); } else { // settled perfectly at bottom s.angle = 0; s.vel = 0; setAngle(0); } } else { // Track velocity while dragging so we can "flick" it if (dt > 0) { s.vel = (s.angle - prevAngleRef.current) / dt; } prevAngleRef.current = s.angle; rafRef.current = requestAnimationFrame(tick); } }, [ropeLength]); const startPhysics = useCallback(() => { if (rafRef.current) cancelAnimationFrame(rafRef.current); prevTimeRef.current = null; rafRef.current = requestAnimationFrame(tick); }, [tick]); // ── Pointer events ────────────────────────────────────────────────────────── const onPointerDown = useCallback((e: React.PointerEvent) => { e.currentTarget.setPointerCapture(e.pointerId); isDraggingRef.current = true; setIsDragState(true); dragStartX.current = e.clientX; dragAngle0.current = physRef.current.angle; prevAngleRef.current = physRef.current.angle; if (rafRef.current) cancelAnimationFrame(rafRef.current); prevTimeRef.current = null; rafRef.current = requestAnimationFrame(tick); }, [tick]); const onPointerMove = useCallback((e: React.PointerEvent) => { if (!isDraggingRef.current) return; const dx = e.clientX - dragStartX.current; const L = ropeLength + 100; const newAngle = dragAngle0.current - dx / L; const clamped = Math.max(-1.4, Math.min(1.4, newAngle)); physRef.current.angle = clamped; setAngle(clamped); }, [ropeLength]); const onPointerUp = useCallback((e: React.PointerEvent) => { e.currentTarget.releasePointerCapture(e.pointerId); isDraggingRef.current = false; setIsDragState(false); }, []); // ── Click impulse (tap) ───────────────────────────────────────────────────── const onCardClick = useCallback(() => { if (Math.abs(physRef.current.vel) < 0.1 && Math.abs(physRef.current.angle) < 0.05) { physRef.current.vel = 4.0; // Give it a satisfying push startPhysics(); } }, [startPhysics]); useEffect(() => () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }, []); const cardRotateDeg = angle * (180 / Math.PI); return (
{/* Ceiling anchor pin */}
{/* The Pendulum Assembly (Rope + Lock Clip + Card) */}
{/* Lanyard Rope with Lock Clip */}
{/* ID Card */}
{/* Punched Slot Hole for Lanyard Clip */}
{children ?? (
{/* Card Header Banner */}
{/* Security Chip Icon */}
{/* User Profile Avatar (No Lightswind Logo) */}
{/* Card Body */}

{name}

{role}

{/* Barcode */}
{Array.from({ length: 26 }).map((_, i) => (
))}

{badgeId}

{/* Status badge */}
ACTIVE
)}
{/* Drag hint */}

Drag or click the card

); }; export default HangingIdCard; ``` -------------------------------------------------- ### COMPONENT: plasma-globe Category: 3D Elements Description: A dynamic and visually striking plasma globe background component built with React and OGL. It renders a fully animated plasma sphere with multiple filaments, supporting real-time mouse interaction, speed, and intensity control. The background is transparent by default, allowing users to overlay any custom background color. URL: https://lightswind.com/components/plasma-globe Import: import PlasmaGlobe from '@/components/PlasmaGlobe'; Registry URL: https://lightswind.com/r/plasma-globe.json Install Command: npx lightswind@latest add plasma-globe Usage: ```tsx import PlasmaGlobe from '@/components/PlasmaGlobe'; ``` Source Code: ```tsx // PlasmaGlobe.tsx "use client"; import React, { useEffect, useRef } from "react"; import { Renderer, Program, Mesh, Triangle } from "ogl"; interface PlasmaGlobeProps { speed?: number; // global time speed multiplier intensity?: number; // color intensity multiplier } const VERTEX_SHADER = `#version 300 es in vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); } `; /* Adapted & simplified plasma globe fragment shader (from Shadertoy-style code). Replaces texture-based noise with small procedural noise functions so it runs without external textures. Uses uniforms: - uTime (float) - uResolution (vec2) - uMouse (vec2) - uSpeed (float) - uIntensity (float) NOTE: keep an eye on precision and performance on low-end GPUs. */ const FRAGMENT_SHADER = `#version 300 es precision highp float; out vec4 fragColor; uniform float uTime; uniform vec2 uResolution; uniform vec2 uMouse; uniform float uSpeed; uniform float uIntensity; #define NUM_RAYS 13.0 #define VOLUMETRIC_STEPS 19 #define MAX_ITER 35 #define FAR 6.0 // small 2x2 rotation matrix mat2 mm2(float a){ float c = cos(a), s = sin(a); return mat2(c, -s, s, c); } // simple hash-based random float hash1(float n){ return fract(sin(n)*43758.5453); } float hash2(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); } // value noise from vec3 -> float (cheap, not high quality but ok) float noise3(vec3 p){ // grid cell vec3 ip = floor(p); vec3 fp = fract(p); fp = fp*fp*(3.0-2.0*fp); float n000 = hash2(ip.xy + ip.z*7.0); float n100 = hash2(ip.xy + vec2(1.0,0.0) + ip.z*7.0); float n010 = hash2(ip.xy + vec2(0.0,1.0) + ip.z*7.0); float n110 = hash2(ip.xy + vec2(1.0,1.0) + ip.z*7.0); float nx0 = mix(n000, n100, fp.x); float nx1 = mix(n010, n110, fp.x); float nxy = mix(nx0, nx1, fp.y); // incorporate z as small modulation using hash float nz = mix(nxy, hash1(ip.z + 1.0), fp.z); return nz; } // light-weight fractal noise (based on noise3) float flow(vec3 p, float t){ float rz = 0.0; vec3 bp = p; float z = 2.0; // a few octaves for (int i = 1; i < 5; i++){ p += t * 0.1; rz += (sin(noise3(p + t*0.8) * 6.0) * 0.5 + 0.5) / z; p = mix(bp, p, 0.6); z *= 2.0; p *= 2.01; p *= mat3( 0.00, 0.80, 0.60, -0.80, 0.36, -0.48, -0.60, -0.48, 0.64 ); } return rz; } // helper used to create wavy variations (low-frequency) float sins(float x, float t){ float rz = 0.0; float z = 2.0; for (int i = 0; i < 3; i++){ rz += abs(fract(x * 1.4) - 0.5) / z; x *= 1.3; z *= 1.15; x -= t * 0.65 * z; } return rz; } float segm(vec3 p, vec3 a, vec3 b){ vec3 pa = p - a; vec3 ba = b - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return length(pa - ba * h) * 0.5; } vec3 path(float i, float d, float t){ // produce a pseudo-random endpoint on unit sphere influenced by i and d float sns2 = sins(d + i * 0.5, t) * 0.22; float sns = sins(d + i * 0.6, t) * 0.21; float a1 = (hash1(i * 10.569) - 0.5) * 6.2 + sns2; float a2 = (hash1(i * 4.732) - 0.5) * 6.2 + sns; vec3 en = vec3(0.0, 0.0, 1.0); en.xz *= mat2(cos(a1), -sin(a1), sin(a1), cos(a1)); en.xy *= mat2(cos(a2), -sin(a2), sin(a2), cos(a2)); return en; } vec2 map(vec3 p, float i, float t){ float lp = length(p); vec3 bg = vec3(0.0); vec3 en = path(i, lp, t); float ins = smoothstep(0.11, 0.46, lp); float outs = 0.15 + smoothstep(0.0, 0.15, abs(lp - 1.0)); p *= ins * outs; float id = ins * outs; float rz = segm(p, bg, en) - 0.011; return vec2(rz, id); } // sphere-ray intersection helper vec2 iSphere2(vec3 ro, vec3 rd){ vec3 oc = ro; float b = dot(oc, rd); float c = dot(oc, oc) - 1.0; float h = b*b - c; if (h < 0.0) return vec2(-1.0); return vec2((-b - sqrt(h)), (-b + sqrt(h))); } // volumetric march (accumulates light along a ray) vec3 vmarch(vec3 ro, vec3 rd, float j, vec3 orig, float t){ vec3 p = ro; vec3 sum = vec3(0.0); for (int i = 0; i < VOLUMETRIC_STEPS; i++){ vec2 r = map(p, j, t); p += rd * 0.03; float lp = length(p); // create color base per-step vec3 col = sin(vec3(1.05, 2.5, 1.52) * 3.94 + r.y) * 0.85 + 0.4; col *= smoothstep(0.0, 0.015, -r.x); col *= smoothstep(0.04, 0.2, abs(lp - 1.1)); col *= smoothstep(0.1, 0.34, lp); // noise modulation float n = noise3(vec3(lp * 2.0 + j * 13.0 + t * 5.0)); // attenuate with distance from origin and add float denom = max(0.0001, log(max(0.0001, distance(p, orig) - 2.0)) + 0.75); sum += abs(col) * 5.0 * (1.2 - n * 1.1) / denom; } return sum; } // ray-marching distance estimator to sphere-like structures float march(vec3 ro, vec3 rd, float startf, float maxd, float j, float t){ float precis = 0.001; float h = 0.5; float d = startf; for (int i = 0; i < MAX_ITER; i++){ if (abs(h) < precis || d > maxd) break; d += h * 1.2; float res = map(ro + rd * d, j, t).x; h = res; } return d; } void main(){ // Normalized coords (-0.5..0.5) vec2 uv = (gl_FragCoord.xy / uResolution.xy) - 0.5; uv.x *= uResolution.x / uResolution.y; vec2 um = (uMouse.xy / uResolution.xy) - 0.5; // camera setup vec3 ro = vec3(0.0, 0.0, 5.0); vec3 rd = normalize(vec3(uv * 0.7, -1.5)); mat2 mx = mm2(uTime * 0.4 + um.x * 6.0); mat2 my = mm2(uTime * 0.3 + um.y * 6.0); ro.xz *= mx; rd.xz *= mx; ro.xy *= my; rd.xy *= my; vec3 bro = ro; vec3 brd = rd; vec3 col = vec3(0.0); // multiple rays to create many filaments for (float j = 1.0; j < NUM_RAYS + 1.0; j++){ ro = bro; rd = brd; mat2 mm = mm2((uTime * 0.1 + ((j + 1.0) * 5.1)) * j * 0.25); ro.xy *= mm; rd.xy *= mm; ro.xz *= mm; rd.xz *= mm; float rz = march(ro, rd, 2.5, FAR, j, uTime); if (rz >= FAR) continue; vec3 pos = ro + rz * rd; col = max(col, vmarch(pos, rd, j, bro, uTime)); } ro = bro; rd = brd; vec2 sph = iSphere2(ro, rd); if (sph.x > 0.0){ vec3 pos = ro + rd * sph.x; vec3 pos2 = ro + rd * sph.y; vec3 rf = reflect(rd, normalize(pos)); vec3 rf2 = reflect(rd, normalize(pos2)); float nz = (-log(abs(flow(rf * 1.2, uTime) - 0.01) + 0.00001)); float nz2 = (-log(abs(flow(rf2 * 1.2, -uTime) - 0.01) + 0.00001)); col += (0.1 * nz * nz * vec3(0.12, 0.12, 0.5) + 0.05 * nz2 * nz2 * vec3(0.55, 0.2, 0.55)) * 0.8; } // final tone mapping & intensity col *= (1.0 + uIntensity * 0.6); col = pow(clamp(col, 0.0, 10.0), vec3(1.5)); float alpha = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0); fragColor = vec4(col * 1.3, alpha); } `; export default function PlasmaGlobe({ speed = 1.0, intensity = 1.0, }: PlasmaGlobeProps) { const containerRef = useRef(null); const mouseRef = useRef({ x: 0, y: 0 }); useEffect(() => { const container = containerRef.current; if (!container) return; // create renderer const renderer = new Renderer({ alpha: true, antialias: true }); const gl = renderer.gl; gl.clearColor(0, 0, 0, 0); gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); // geometry const geometry = new Triangle(gl); // program const program = new Program(gl, { vertex: VERTEX_SHADER, fragment: FRAGMENT_SHADER, uniforms: { uTime: { value: 0 }, uResolution: { value: [container.offsetWidth, container.offsetHeight] }, uMouse: { value: [0, 0] }, uSpeed: { value: speed }, uIntensity: { value: intensity }, }, }); const mesh = new Mesh(gl, { geometry, program }); container.appendChild(gl.canvas); // resize const resize = () => { const width = container.offsetWidth; const height = container.offsetHeight; renderer.setSize(width, height); program.uniforms.uResolution.value = [width, height]; }; window.addEventListener("resize", resize); resize(); // mouse smoothing const onMouse = (e: MouseEvent) => { mouseRef.current.x += (e.clientX - mouseRef.current.x) * 0.08; mouseRef.current.y += (e.clientY - mouseRef.current.y) * 0.08; }; window.addEventListener("mousemove", onMouse); let rafId = 0; const loop = (t: number) => { rafId = requestAnimationFrame(loop); // uTime passed in seconds, multiplied by speed program.uniforms.uTime.value = (t * 0.001) * speed; program.uniforms.uMouse.value = [mouseRef.current.x, mouseRef.current.y]; program.uniforms.uIntensity.value = intensity; renderer.render({ scene: mesh }); }; rafId = requestAnimationFrame(loop); return () => { cancelAnimationFrame(rafId); window.removeEventListener("resize", resize); window.removeEventListener("mousemove", onMouse); if (gl.canvas.parentNode === container) container.removeChild(gl.canvas); gl.getExtension("WEBGL_lose_context")?.loseContext(); }; }, [speed, intensity]); return (
); } ``` -------------------------------------------------- ### COMPONENT: stylish-carousel Category: 3D Elements Description: A visually stunning image carousel built with Framer Motion spring physics. Slides fan out in a rotating, scaled perspective giving a cinematic fan-out effect. Fully keyboard-accessible, swipe-enabled on touch devices, supports auto-play, click-to-navigate, dot indicators, directional arrow controls, and extensive prop customisation. URL: https://lightswind.com/components/stylish-carousel Import: import StylishCarousel from "@/components/lightswind/stylish-carousel" Registry URL: https://lightswind.com/r/stylish-carousel.json Install Command: npx lightswind@latest add stylish-carousel Usage: ```tsx import StylishCarousel from "@/components/lightswind/stylish-carousel"; const items = [ { src: "https://images.unsplash.com/photo-1234...", title: "Mountain View" }, { src: "https://images.unsplash.com/photo-5678...", title: "Seascape" }, { src: "https://images.unsplash.com/photo-9012...", title: "City Lights" }, ]; export function CarouselDemo() { return ( console.log("Active:", i)} /> ); } ``` Source Code: ```tsx "use client"; import React, { useState, useCallback, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { cn } from "../lib/utils"; // ───────────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────────── export interface StylishCarouselItem { src: string; title?: string; alt?: string; } export interface StylishCarouselProps { /** Array of items to display in the carousel */ items: StylishCarouselItem[]; /** Starting active index (0-based) */ initialIndex?: number; /** Size of each slide (CSS clamp value or fixed px). Defaults to responsive clamp. */ slideSize?: string; /** Rotation angle (degrees) applied per offset position */ rotationDegrees?: number; /** Scale of non-active slides (0–1). Defaults to 0.6 */ inactiveScale?: number; /** Y-offset multiplier for perspective fan-out. Defaults to 50 */ yOffsetPercent?: number; /** Spring animation bounce (0–1). Defaults to 0.15 */ springBounce?: number; /** Spring animation duration in seconds. Defaults to 0.8 */ springDuration?: number; /** Whether to show navigation arrows */ showArrows?: boolean; /** Whether to show dot indicators */ showDots?: boolean; /** Whether images are clickable to navigate to that slide */ clickToNavigate?: boolean; /** Auto-advance interval in ms. 0 = disabled */ autoPlay?: number; /** Additional className for the root wrapper */ className?: string; /** Callback fired when the active index changes */ onIndexChange?: (index: number) => void; /** Border radius of each slide image. Defaults to "1rem" */ borderRadius?: string; /** Custom dot active color (Tailwind or CSS color) */ dotActiveColor?: string; /** Custom dot inactive color */ dotInactiveColor?: string; /** Custom arrow button class override */ arrowClassName?: string; } // ───────────────────────────────────────────────────────────────────────────── // Component // ───────────────────────────────────────────────────────────────────────────── const StylishCarousel = ({ items = [], initialIndex = 0, slideSize = "clamp(140px, 75vmin, 320px)", rotationDegrees = 28, inactiveScale = 0.62, yOffsetPercent = 48, springBounce = 0.15, springDuration = 0.8, showArrows = true, showDots = true, clickToNavigate = true, autoPlay = 0, className, onIndexChange, borderRadius = "1rem", arrowClassName, }: StylishCarouselProps) => { const clampedInitial = Math.max(0, Math.min(initialIndex, items.length - 1)); const [activeIndex, setActiveIndex] = useState(clampedInitial); const autoPlayRef = useRef | null>(null); const containerRef = useRef(null); // ── helpers ────────────────────────────────────────────────────────────── const goTo = useCallback( (index: number) => { const clamped = Math.max(0, Math.min(index, items.length - 1)); setActiveIndex(clamped); onIndexChange?.(clamped); }, [items.length, onIndexChange] ); const toPrev = useCallback( () => goTo(activeIndex - 1), [activeIndex, goTo] ); const toNext = useCallback( () => goTo(activeIndex + 1), [activeIndex, goTo] ); // ── keyboard navigation ─────────────────────────────────────────────────── useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "ArrowLeft") toPrev(); if (e.key === "ArrowRight") toNext(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [toPrev, toNext]); // ── touch / swipe ───────────────────────────────────────────────────────── const touchStartX = useRef(null); const handleTouchStart = (e: React.TouchEvent) => { touchStartX.current = e.touches[0].clientX; }; const handleTouchEnd = (e: React.TouchEvent) => { if (touchStartX.current === null) return; const delta = touchStartX.current - e.changedTouches[0].clientX; if (Math.abs(delta) > 40) delta > 0 ? toNext() : toPrev(); touchStartX.current = null; }; // ── auto-play ───────────────────────────────────────────────────────────── useEffect(() => { if (!autoPlay) return; autoPlayRef.current = setInterval(() => { setActiveIndex((prev) => { const next = prev + 1 >= items.length ? 0 : prev + 1; onIndexChange?.(next); return next; }); }, autoPlay); return () => { if (autoPlayRef.current) clearInterval(autoPlayRef.current); }; }, [autoPlay, items.length, onIndexChange]); // ── spring transition ────────────────────────────────────────────────────── const spring = { type: "spring" as const, bounce: springBounce, duration: springDuration, }; if (!items.length) return null; return (
{/* ── SLIDES CONTAINER ─────────────────────────────────────────────── */}
{/* Horizontal sliding strip */} {items.map((item, i) => { const offset = i - activeIndex; const isActive = offset === 0; return ( {/* Title label */} {item.title && ( {item.title} )} {/* Image */}
{item.alt clickToNavigate && goTo(i)} className={cn( "w-full h-full object-cover transition-[filter] duration-300 will-change-transform", !isActive && "brightness-75", clickToNavigate && !isActive && "cursor-pointer" )} loading="lazy" /> {/* Active glow ring */} {isActive && ( )}
); })}
{/* ── CONTROLS ─────────────────────────────────────────────────────── */}
{/* Prev */} {showArrows && ( )} {/* Dots */} {showDots && (
{items.map((_, i) => ( goTo(i)} animate={{ width: activeIndex === i ? 28 : 8, opacity: activeIndex === i ? 1 : 0.35, }} transition={{ type: "spring", bounce: 0.3, duration: 0.5 }} className="h-2 rounded-full bg-foreground cursor-pointer" style={{ minWidth: 8 }} /> ))}
)} {/* Next */} {showArrows && ( )}
{/* Counter */}

{activeIndex + 1} / {items.length}

); }; export default StylishCarousel; ``` -------------------------------------------------- ### COMPONENT: cool-slide-gallery Category: 3D Elements Description: A premium 3D coverflow-style image gallery built with Framer Motion. Cards fan out in a smooth perspective layout with configurable tilt, depth, scale, and rotation. Supports drag/swipe, keyboard navigation, autoplay, dot indicators, navigation arrows, title overlays with gradient veils, badge labels, slide counter, and extensive customisation. Fully transparent background with seamless light and dark mode support. URL: https://lightswind.com/components/cool-slide-gallery Import: import CoolSlideGallery from "@/components/lightswind/cool-slide-gallery" Registry URL: https://lightswind.com/r/cool-slide-gallery.json Install Command: npx lightswind@latest add cool-slide-gallery Usage: ```tsx import CoolSlideGallery from "@/components/lightswind/cool-slide-gallery"; const slides = [ { src: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800", title: "Alpine Summit", subtitle: "Swiss Alps, 2024", badge: "Featured", }, { src: "https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?w=800", title: "Forest Trail", subtitle: "Oregon, USA", badge: "Nature", }, ]; export function Demo() { return (
console.log("Active:", slide.title)} />
); } ``` Source Code: ```tsx "use client"; import React, { useState, useEffect, useCallback, useRef } from "react"; import { motion, useInView } from "framer-motion"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { cn } from "../lib/utils"; // ───────────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────────── export interface CoolSlideGallerySlide { /** Image source URL */ src: string; /** Optional alt text for accessibility */ alt?: string; /** Optional title rendered as an overlay on the card */ title?: string; /** Optional subtitle text shown below the title */ subtitle?: string; /** Optional custom badge text */ badge?: string; } export type TitlePosition = "bottom-left" | "bottom-right" | "top-left" | "top-right" | "center"; export type EasingPreset = "smooth" | "spring" | "bouncy" | "snappy"; export interface CoolSlideGalleryProps { /** Array of slides to display */ slides: CoolSlideGallerySlide[]; /** Card width in pixels */ cardWidth?: number; /** Card height in pixels */ cardHeight?: number; /** Border radius scale 0–20 (maps to pixel radius relative to card size) */ radius?: number; /** Y-rotation angle (degrees) applied to side cards */ tilt?: number; /** Z-rotation (degrees) applied to side cards for slight lean */ sideTilt?: number; /** Horizontal gap multiplier between cards */ gap?: number; /** Dim intensity (0–100) applied to non-active cards */ dimOpacity?: number; /** Whether to enable autoplay */ autoplay?: boolean; /** Direction of autoplay progression */ autoplayDirection?: "left-to-right" | "right-to-left"; /** Delay in seconds between autoplay slides */ autoplayDelay?: number; /** Animation duration in seconds for slide transitions */ animationDuration?: number; /** Easing preset for the slide animation */ easing?: EasingPreset; /** Whether to show slide titles */ showTitle?: boolean; /** Position of the title overlay */ titlePosition?: TitlePosition; /** Whether to show navigation arrow buttons */ showArrows?: boolean; /** Whether to show dot indicators */ showDots?: boolean; /** Whether to show a slide counter */ showCounter?: boolean; /** Whether to show badge labels */ showBadge?: boolean; /** Whether click on side cards navigates to them */ clickable?: boolean; /** Whether drag/swipe is enabled */ draggable?: boolean; /** Minimum drag distance in px to trigger a slide change */ dragThreshold?: number; /** Whether keyboard navigation is enabled */ keyboardNavigation?: boolean; /** Number of side cards visible on each side */ maxVisible?: number; /** Depth (z-translate in px) applied per offset position */ depth?: number; /** Scale reduction per offset step from center */ scaleStep?: number; /** Perspective value for the 3D scene */ perspective?: number; /** Additional CSS classes on the root wrapper */ className?: string; /** Callback fired when the active index changes */ onSlideChange?: (index: number, slide: CoolSlideGallerySlide) => void; } // ───────────────────────────────────────────────────────────────────────────── // Constants & Defaults // ───────────────────────────────────────────────────────────────────────────── const DEFAULT_SLIDES: CoolSlideGallerySlide[] = [ { src: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop", alt: "Mountain peaks at golden hour", title: "Alpine Summit", subtitle: "Swiss Alps, 2024", badge: "Featured", }, { src: "https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?w=800&auto=format&fit=crop", alt: "Misty forest trail", title: "Forest Trail", subtitle: "Oregon, USA", badge: "Nature", }, { src: "https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop", alt: "Starry night over mountains", title: "Midnight Sky", subtitle: "Patagonia, Chile", badge: "Astro", }, { src: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&auto=format&fit=crop", alt: "Desert dunes at sunset", title: "Sand Waves", subtitle: "Sahara Desert", badge: "Desert", }, { src: "https://images.unsplash.com/photo-1505118380757-91f5f5632de0?w=800&auto=format&fit=crop", alt: "Tropical ocean shore", title: "Ocean Horizon", subtitle: "Maldives", badge: "Ocean", }, ]; const EASING_MAP: Record = { smooth: [0.22, 1, 0.36, 1], spring: [0.34, 1.56, 0.64, 1], bouncy: [0.5, 1.7, 0.5, 1], snappy: [0.16, 1, 0.3, 1], }; // ───────────────────────────────────────────────────────────────────────────── // Utility: title position styles // ───────────────────────────────────────────────────────────────────────────── function getTitleStyles(position: TitlePosition): React.CSSProperties { const base: React.CSSProperties = { position: "absolute", pointerEvents: "none", }; switch (position) { case "bottom-left": return { ...base, bottom: 0, left: 0, right: 0 }; case "bottom-right": return { ...base, bottom: 0, right: 0, textAlign: "right" }; case "top-left": return { ...base, top: 0, left: 0, right: 0 }; case "top-right": return { ...base, top: 0, right: 0, textAlign: "right" }; case "center": return { ...base, top: "50%", left: "50%", transform: "translate(-50%, -50%)", textAlign: "center", width: "100%", }; default: return { ...base, bottom: 0, left: 0, right: 0 }; } } function getGradientForPosition(position: TitlePosition): string { switch (position) { case "top-left": case "top-right": return "linear-gradient(180deg, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 60%)"; case "center": return "radial-gradient(ellipse at center, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 70%)"; case "bottom-left": case "bottom-right": default: return "linear-gradient(0deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 65%)"; } } // ───────────────────────────────────────────────────────────────────────────── // CoolSlideGallery Component // ───────────────────────────────────────────────────────────────────────────── const CoolSlideGallery: React.FC = ({ slides: slidesProp, cardWidth = 380, cardHeight = 440, radius = 5, tilt = 14, sideTilt = 6, gap = 8, dimOpacity = 55, autoplay = false, autoplayDirection = "right-to-left", autoplayDelay = 2.8, animationDuration = 0.6, easing = "smooth", showTitle = true, titlePosition = "bottom-left", showArrows = true, showDots = true, showCounter = false, showBadge = true, clickable = true, draggable = true, dragThreshold = 45, keyboardNavigation = true, maxVisible = 2, depth = 230, scaleStep = 0.15, perspective = 1500, className, onSlideChange, }) => { const slides = slidesProp && slidesProp.length > 0 ? slidesProp : DEFAULT_SLIDES; const n = slides.length; const [active, setActive] = useState(0); const lockRef = useRef(false); const dragStartX = useRef(0); const isDragging = useRef(false); const containerRef = useRef(null); const isInView = useInView(containerRef, { margin: "200px" }); // ── Locking ────────────────────────────────────────────────────────────── const lock = useCallback(() => { lockRef.current = true; window.setTimeout(() => { lockRef.current = false; }, Math.max(50, animationDuration * 1000)); }, [animationDuration]); // ── Step ───────────────────────────────────────────────────────────────── const step = useCallback( (dir: 1 | -1) => { if (lockRef.current) return; lock(); setActive((a) => { const next = (((a + dir) % n) + n) % n; onSlideChange?.(next, slides[next]); return next; }); }, [n, lock, onSlideChange, slides] ); const goTo = useCallback( (i: number) => { if (lockRef.current || i === active) return; lock(); setActive(i); onSlideChange?.(i, slides[i]); }, [active, lock, onSlideChange, slides] ); // ── Keyboard ───────────────────────────────────────────────────────────── useEffect(() => { if (!keyboardNavigation) return; const onKey = (e: KeyboardEvent) => { if (e.key === "ArrowRight") { e.preventDefault(); step(1); } if (e.key === "ArrowLeft") { e.preventDefault(); step(-1); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [step, keyboardNavigation]); // ── Autoplay (pauses when out of view) ────────────────────────────────── useEffect(() => { if (!autoplay || n < 2 || !isInView) return; const ms = Math.max(300, autoplayDelay * 1000); const dir: 1 | -1 = autoplayDirection === "left-to-right" ? -1 : 1; const id = window.setInterval(() => step(dir), ms); return () => window.clearInterval(id); }, [autoplay, autoplayDirection, autoplayDelay, n, step, isInView]); // ── Pointer drag ───────────────────────────────────────────────────────── const handlePointerDown = (e: React.PointerEvent) => { if (!draggable || lockRef.current) return; isDragging.current = true; dragStartX.current = e.clientX; (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); }; const handlePointerUp = (e: React.PointerEvent) => { if (!isDragging.current) return; isDragging.current = false; (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); const delta = e.clientX - dragStartX.current; if (Math.abs(delta) > dragThreshold) { step(delta > 0 ? -1 : 1); } }; // ── Touch ───────────────────────────────────────────────────────────────── const touchStartX = useRef(null); const handleTouchStart = (e: React.TouchEvent) => { touchStartX.current = e.touches[0].clientX; }; const handleTouchEnd = (e: React.TouchEvent) => { if (touchStartX.current === null) return; const delta = touchStartX.current - e.changedTouches[0].clientX; if (Math.abs(delta) > dragThreshold) step(delta > 0 ? 1 : -1); touchStartX.current = null; }; // ── Animation bezier ───────────────────────────────────────────────────── const [x1, y1, x2, y2] = EASING_MAP[easing]; const transition = { type: "tween" as const, duration: animationDuration, ease: [x1, y1, x2, y2] as [number, number, number, number], }; // ── Derived values ──────────────────────────────────────────────────────── const effectiveRadius = (Math.max(0, Math.min(20, radius)) / 20) * (Math.min(cardWidth, cardHeight) / 2); const dimValue = 1 - Math.max(0, Math.min(100, dimOpacity)) / 100; const isTopPosition = titlePosition === "top-left" || titlePosition === "top-right"; return (
{/* ── 3D Stage ───────────────────────────────────────────────────────── */}
{slides.map((slide, i) => { let rel = i - active; // Wrap for infinite loop if (rel > n / 2) rel -= n; if (rel < -n / 2) rel += n; const ax = Math.abs(rel); const visible = ax <= maxVisible; const isActive = rel === 0; const sc = Math.max(0.3, 1 - ax * scaleStep); const tx = rel * (gap * 30); const tz = -ax * depth; const ry = -rel * tilt; const rz = rel * sideTilt; return ( { if (clickable && !isDragging.current && !isActive && visible) { goTo(i); } }} aria-label={slide.title ?? slide.alt ?? `Slide ${i + 1}`} aria-hidden={!visible} > {/* Image */} {slide.alt {/* Title overlay */} {showTitle && (slide.title || slide.subtitle) && ( <> {/* Gradient veil */}
{/* Text content */}
{slide.badge && showBadge && ( {slide.badge} )} {slide.title && (

{slide.title}

)} {slide.subtitle && (

{slide.subtitle}

)}
)} {/* Dim overlay on non-active cards */} ); })}
{/* ── Controls bar ───────────────────────────────────────────────────── */}
e.stopPropagation()} onPointerUp={(e) => e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()} onTouchEnd={(e) => e.stopPropagation()} > {/* Prev */} {showArrows && ( { e.stopPropagation(); step(-1); }} className={cn( "p-2 rounded-full transition-colors", "text-foreground/60 hover:text-foreground hover:bg-foreground/10" )} whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} > )} {/* Dots */} {showDots && (
{slides.map((_, i) => ( { e.stopPropagation(); goTo(i); }} className="rounded-full bg-foreground/60 dark:bg-foreground/50 cursor-pointer transition-colors hover:bg-foreground" animate={{ width: active === i ? 24 : 6, height: 6, opacity: active === i ? 1 : 0.4, }} transition={{ type: "spring", bounce: 0.3, duration: 0.45 }} style={{ minWidth: 6 }} /> ))}
)} {/* Counter */} {showCounter && ( {active + 1} / {n} )} {/* Next */} {showArrows && ( { e.stopPropagation(); step(1); }} className={cn( "p-2 rounded-full transition-colors", "text-foreground/60 hover:text-foreground hover:bg-foreground/10" )} whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} > )}
); }; export default CoolSlideGallery; ``` -------------------------------------------------- ### COMPONENT: scroll-carousel Category: 3D Elements Description: A responsive and animated scrollable carousel that displays feature cards. On desktop, it provides smooth horizontal scrolling with GSAP + ScrollTrigger pinning and progress tracking. On mobile, it animates cards vertically with fade-in and slide effects. Supports dynamic rows, custom max scroll height, and animated progress bar. URL: https://lightswind.com/components/scroll-carousel Import: import ScrollCarousel from '@/components/lightswind/scroll-carousel'; Registry URL: https://lightswind.com/r/scroll-carousel.json Install Command: npx lightswind@latest add scroll-carousel Usage: ```tsx import ScrollCarousel from '@/components/lightswind/scroll-carousel'; const features = [ { icon: Send, title: "Instant Payments", description: "Send money anywhere in the world instantly.", image: "https://images.pexels.com/photos/9934462/pexels-photo-9934462.jpeg", }, { icon: Globe, title: "Global Access", description: "Access your wallet from any device worldwide.", image: "https://images.pexels.com/photos/6988085/pexels-photo-6988085.jpeg", }, { icon: Shield, title: "Secure Transactions", description: "Your payments are protected with top-level security.", image: "https://images.pexels.com/photos/6863184/pexels-photo-6863184.jpeg", }, ]; ``` Source Code: ```tsx "use client"; import React, { useEffect, useRef, useState, useLayoutEffect, forwardRef, } from "react"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import { LucideIcon } from "lucide-react"; // Assuming these are external, import them import { cn } from "@/components/lib/utils"; gsap.registerPlugin(ScrollTrigger); // --- Component Props and Types --- // Define a type for a single feature object export interface FeatureItem { icon: LucideIcon; title: string; description: string; image: string; } // Define the component's props interface export interface ScrollCarouselProps { features: FeatureItem[]; className?: string; // To allow external classes maxScrollHeight?: number; // New optional prop for max scroll height } // --- Custom Hook for Animations --- const useFeatureAnimations = ( containerRef: React.RefObject, scrollContainerRef: React.RefObject, scrollContainerRef2: React.RefObject, progressBarRef: React.RefObject, cardRefs: React.MutableRefObject, cardRefs2: React.MutableRefObject, isDesktop: boolean, maxScrollHeight?: number ) => { useLayoutEffect(() => { let ctx = gsap.context(() => { // Desktop horizontal scroll logic if (isDesktop) { const scrollWidth1 = scrollContainerRef.current?.scrollWidth || 0; const scrollWidth2 = scrollContainerRef2.current?.scrollWidth || 0; const containerWidth = containerRef.current?.offsetWidth || 0; const cardWidth = cardRefs.current[0]?.offsetWidth || 0; const viewportOffset = (containerWidth - cardWidth) / 2; const finalOffset1 = scrollWidth1 - containerWidth + viewportOffset; const finalOffset2 = scrollWidth2 - containerWidth + viewportOffset; // Use the provided maxScrollHeight or the calculated offset as the scroll distance const scrollDistance = maxScrollHeight || finalOffset1; gsap.set(scrollContainerRef2.current, { x: -finalOffset2 + viewportOffset * 2, }); gsap .timeline({ scrollTrigger: { trigger: containerRef.current, start: "top top", end: () => `+=${scrollDistance}`, scrub: 1, pin: true, }, }) .fromTo( scrollContainerRef.current, { x: viewportOffset }, { x: -finalOffset1 + viewportOffset, ease: "none" } ); gsap .timeline({ scrollTrigger: { trigger: containerRef.current, start: "top top", end: () => `+=${scrollDistance}`, scrub: 1, }, }) .to(scrollContainerRef2.current, { x: viewportOffset, ease: "none" }); gsap.to(progressBarRef.current, { width: "100%", ease: "none", scrollTrigger: { trigger: containerRef.current, start: "top top", end: () => `+=${scrollDistance}`, scrub: true, }, }); } else { // Mobile vertical scroll logic const allCards = [...cardRefs.current, ...cardRefs2.current]; allCards.forEach((card, index) => { if (card) { gsap.fromTo( card, { opacity: 0, x: index % 2 === 0 ? -200 : 200, }, { opacity: 1, x: 0, duration: 1, ease: "power2.out", scrollTrigger: { trigger: card, start: "top 0%", toggleActions: "play none none none", once: true, }, } ); } }); } }, containerRef); return () => { ctx.revert(); }; }, [isDesktop, maxScrollHeight]); }; // --- Component Definition --- export const ScrollCarousel = forwardRef( ({ features, className, maxScrollHeight }, ref) => { const containerRef = useRef(null); const scrollContainerRef = useRef(null); const scrollContainerRef2 = useRef(null); const progressBarRef = useRef(null); const cardRefs = useRef([]); const cardRefs2 = useRef([]); const [isDesktop, setIsDesktop] = useState(false); // Dynamic sorting for the second row of cards const features2 = [...features].sort(() => Math.random() - 0.5); useEffect(() => { const checkDesktop = () => { setIsDesktop(window.matchMedia("(min-width: 768px)").matches); }; checkDesktop(); window.addEventListener("resize", checkDesktop); return () => window.removeEventListener("resize", checkDesktop); }, []); useFeatureAnimations( containerRef, scrollContainerRef, scrollContainerRef2, progressBarRef, cardRefs, cardRefs2, isDesktop, maxScrollHeight ); const renderFeatureCards = ( featureSet: FeatureItem[], refs: React.MutableRefObject ) => featureSet.map((feature, index) => (
{ if (el) refs.current[index] = el; }} className="feature-card flex-shrink-0 w-full md:w-full h-full z-10 gap-4 group relative transition-all duration-300 ease-in-out" >
{/* */}

{feature.title}

{feature.description}

{/* */}
)); return (
{renderFeatureCards(features, cardRefs)}
{isDesktop && (
)}
); } ); ScrollCarousel.displayName = "ScrollCarousel"; export default ScrollCarousel; ``` -------------------------------------------------- ### COMPONENT: sparkle-navbar Category: 3D Elements Description: A reusable, animated navigation menu component built with React, TypeScript, and GSAP. It features a dynamic active state indicator with a glowing strike-through effect, smooth transitions, and customizable colors. Ideal for modern websites needing interactive navigation with engaging micro-animations. URL: https://lightswind.com/components/sparkle-navbar Import: import SparkleNavbar from '@/components/lightswind/sparkle-navbar'; Registry URL: https://lightswind.com/r/sparkle-navbar.json Install Command: npx lightswind@latest add sparkle-navbar Usage: ```tsx import SparkleNavbar from '@/components/lightswind/sparkle-navbar'; ``` Source Code: ```tsx import React, { useState, useRef, useLayoutEffect } from "react"; import { gsap } from "gsap"; // Define the props for the reusable component. interface SparkleNavbarProps { /** * An array of strings representing the navigation menu items. * Each string will be the text for a button. * @example ['Home', 'About', 'Contact'] */ items: string[]; /** * The color for the active state text shadow, box shadow, and other effects. * @example '#1E90FF' (a shade of blue) */ color?: string; } /** * A reusable navigation menu component with a dynamic, animated active state indicator. * All CSS and animation logic are self-contained within this single TSX file. * * @param {SparkleNavbarProps} props - The component props. * @returns {JSX.Element} The rendered navigation menu. */ const SparkleNavbar: React.FC = ({ items, color = "#00fffc", }) => { const [activeIndex, setActiveIndex] = useState(0); // Refs to get direct access to DOM elements for animations. const navRef = useRef(null); const activeElementRef = useRef(null); const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); // Function to create the SVG content for the active element. const createSVG = (element: HTMLDivElement) => { element.innerHTML = `
`; }; // Helper function to calculate the horizontal offset for the active element. const getOffsetLeft = (element: HTMLButtonElement) => { if (!navRef.current || !activeElementRef.current) return 0; const elementRect = element.getBoundingClientRect(); const navRect = navRef.current.getBoundingClientRect(); const activeElementWidth = activeElementRef.current.offsetWidth; return ( elementRect.left - navRect.left + (elementRect.width - activeElementWidth) / 2 ); }; // useLayoutEffect runs synchronously after all DOM mutations, ensuring the // initial position of the active element is correct before the first paint. useLayoutEffect(() => { const activeButton = buttonRefs.current[activeIndex]; if (navRef.current && activeElementRef.current && activeButton) { gsap.set(activeElementRef.current, { x: getOffsetLeft(activeButton), }); gsap.to(activeElementRef.current, { "--active-element-show": "1", duration: 0.2, }); } }, []); // Handler for button clicks, which triggers the animation. const handleClick = (index: number) => { const navElement = navRef.current; const activeElement = activeElementRef.current; const oldButton = buttonRefs.current[activeIndex]; const newButton = buttonRefs.current[index]; if ( index === activeIndex || !navElement || !activeElement || !oldButton || !newButton ) { return; } const x = getOffsetLeft(newButton); const direction = index > activeIndex ? "after" : "before"; const spacing = Math.abs(x - getOffsetLeft(oldButton)); navElement.classList.add(direction); gsap.set(activeElement, { rotateY: direction === "before" ? "180deg" : "0deg", }); gsap.to(activeElement, { keyframes: [ { "--active-element-width": `${spacing > navElement.offsetWidth - 60 ? navElement.offsetWidth - 60 : spacing}px`, duration: 0.3, ease: "none", onStart: () => { createSVG(activeElement); gsap.to(activeElement, { "--active-element-opacity": 1, duration: 0.1, }); }, }, { "--active-element-scale-x": "0", "--active-element-scale-y": ".25", "--active-element-width": "0px", duration: 0.3, onStart: () => { gsap.to(activeElement, { "--active-element-mask-position": "40%", duration: 0.5, }); gsap.to(activeElement, { "--active-element-opacity": 0, delay: 0.45, duration: 0.25, }); }, onComplete: () => { activeElement.innerHTML = ""; navElement.classList.remove("before", "after"); gsap.set(activeElement, { x: getOffsetLeft(newButton), "--active-element-show": "1", }); // Update the state after the animation completes to trigger a re-render // with the new active item. setActiveIndex(index); }, }, ], }); gsap.to(activeElement, { x, "--active-element-strike-x": "-50%", duration: 0.6, ease: "none", }); }; return ( <> {/* The main container for the component, replicating the body styles. */} ); }; export default SparkleNavbar; ``` -------------------------------------------------- ## CATEGORY (FREE): AI Components (2) ### COMPONENT: ai-gooey-blob Category: AI Components Description: A mesmerising SVG liquid metaball gooey blob loader and AI state visualizer. Uses SVG feGaussianBlur and feColorMatrix filters to fuse satellite liquid droplets into a central glowing nucleus. Features orbit, pulse morph, wave flow, and cursor-following interactive animation modes. URL: https://lightswind.com/components/ai-gooey-blob Import: import { AiGooeyBlob } from "@/components/lightswind/ai-gooey-blob" Registry URL: https://lightswind.com/r/ai-gooey-blob.json Install Command: npx lightswind@latest add ai-gooey-blob Usage: ```tsx import { AiGooeyBlob } from "@/components/lightswind/ai-gooey-blob"; export function Demo() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useId } from "react"; import { motion } from "framer-motion"; import { MicOff } from "lucide-react"; export interface AiGooeyBlobProps extends React.HTMLAttributes { /** Diameter of the AI Blob container in pixels (default: 220) */ size?: number; /** Primary gradient color (default: "#6366f1") */ color?: string; /** Secondary gradient color (default: "#ec4899") */ colorSecondary?: string; /** Tertiary gradient color (default: "#06b6d4") */ colorTertiary?: string; /** AI Assistant state mode: "listening" | "thinking" | "speaking" | "orbit" | "pulse" */ variant?: "listening" | "thinking" | "speaking" | "orbit" | "pulse"; /** Sensitivity scale modifier (default: 0.6) */ audioLevel?: number; /** Active listening state toggle */ isListening?: boolean; /** Click handler for central mic orb */ onMicClick?: () => void; /** Custom icon or content inside the core nucleus */ centerContent?: React.ReactNode; } export const AiGooeyBlob: React.FC = ({ size = 220, color = "#6366f1", colorSecondary = "#ec4899", colorTertiary = "#06b6d4", variant = "listening", audioLevel = 0.6, isListening = true, onMicClick, centerContent, className = "", style, ...props }) => { const rawId = useId(); const filterId = `gooey-filter-${rawId.replace(/:/g, "")}`; return (
{/* ── SVG Crisp Gooey Liquid Filter Definition ── */} {/* ── Liquid Gooey Metaball Container ── */}
{/* Central Core Liquid Nucleus - Pure Fluid Morphing (No Line Animations) */} {/* Orbiting Satellite Liquid Drops - Deep Fluid Fusion */} {[0, 1, 2, 3, 4].map((i) => { const angle = (i * 360) / 5; const orbitRadius = size * (0.24 + (i % 2) * 0.05); const dropSize = size * (0.2 + (i % 3) * 0.04); return ( ); })}
{/* ── Center Micro-Interactive Mic / Voice Orb with 5 Mode-Specific 3-Bar Equalizers ── */}
{centerContent ? ( centerContent ) : isListening ? (
{/* 1. LISTENING MODE: Dynamic Audio Mic Equalizer Bounce */} {variant === "listening" && ( <> )} {/* 2. THINKING MODE: Floating Loading Wave Dots */} {variant === "thinking" && ( <> )} {/* 3. SPEAKING MODE: High Energy Rapid Speech Oscillation */} {variant === "speaking" && ( <> )} {/* 4. ORBIT MODE: Rotating 3-Dot Orbital Loop */} {variant === "orbit" && ( )} {/* 5. PULSE MODE: Synchronized Breathing Equalizer Pulse */} {variant === "pulse" && ( <> )}
) : ( )}
); }; export default AiGooeyBlob; ``` -------------------------------------------------- ### COMPONENT: ai-aurora-blob Category: AI Components Description: A gorgeous WebGL OGL 3D Perlin noise Soft Aurora fluid wave sphere component with real-time mouse interaction and customizable cosine gradients for AI voice assistants. URL: https://lightswind.com/components/ai-aurora-blob Import: import { AiAuroraBlob } from "@/components/lightswind/ai-aurora-blob" Registry URL: https://lightswind.com/r/ai-aurora-blob.json Install Command: npx lightswind@latest add ai-aurora-blob Usage: ```tsx import { AiAuroraBlob } from "@/components/lightswind/ai-aurora-blob"; export function Demo() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useEffect, useRef, useState } from "react"; import { Renderer, Program, Mesh, Triangle } from "ogl"; import { motion } from "framer-motion"; import { MicOff } from "lucide-react"; function hexToVec3(hex: string): [number, number, number] { const h = hex.replace("#", ""); return [ parseInt(h.slice(0, 2), 16) / 255 || 0, parseInt(h.slice(2, 4), 16) / 255 || 0, parseInt(h.slice(4, 6), 16) / 255 || 0, ]; } const vertexShader = `#version 300 es in vec2 uv; in vec2 position; out vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 0, 1); } `; const fragmentShader = `#version 300 es precision highp float; uniform float uTime; uniform vec3 uResolution; uniform float uSpeed; uniform float uScale; uniform float uBrightness; uniform vec3 uColor1; uniform vec3 uColor2; uniform float uNoiseFreq; uniform float uNoiseAmp; uniform float uBandHeight; uniform float uBandSpread; uniform float uOctaveDecay; uniform float uLayerOffset; uniform float uColorSpeed; uniform vec2 uMouse; uniform float uMouseInfluence; uniform bool uEnableMouse; uniform bool uShowBorder; out vec4 fragColor; #define TAU 6.28318530718 vec3 gradientHash(vec3 p) { p = vec3( dot(p, vec3(127.1, 311.7, 234.6)), dot(p, vec3(269.5, 183.3, 198.3)), dot(p, vec3(169.5, 283.3, 156.9)) ); vec3 h = fract(sin(p) * 43758.5453123); float phi = acos(2.0 * h.x - 1.0); float theta = TAU * h.y; return vec3(cos(theta) * sin(phi), sin(theta) * cos(phi), cos(phi)); } float quinticSmooth(float t) { float t2 = t * t; float t3 = t * t2; return 6.0 * t3 * t2 - 15.0 * t2 * t2 + 10.0 * t3; } vec3 cosineGradient(float t, vec3 a, vec3 b, vec3 c, vec3 d) { return a + b * cos(TAU * (c * t + d)); } float perlin3D(float amplitude, float frequency, float px, float py, float pz) { float x = px * frequency; float y = py * frequency; float fx = floor(x); float fy = floor(y); float fz = floor(pz); float cx = ceil(x); float cy = ceil(y); float cz = ceil(pz); vec3 g000 = gradientHash(vec3(fx, fy, fz)); vec3 g100 = gradientHash(vec3(cx, fy, fz)); vec3 g010 = gradientHash(vec3(fx, cy, fz)); vec3 g110 = gradientHash(vec3(cx, cy, fz)); vec3 g001 = gradientHash(vec3(fx, fy, cz)); vec3 g101 = gradientHash(vec3(cx, fy, cz)); vec3 g011 = gradientHash(vec3(fx, cy, cz)); vec3 g111 = gradientHash(vec3(cx, cy, cz)); float d000 = dot(g000, vec3(x - fx, y - fy, pz - fz)); float d100 = dot(g100, vec3(x - cx, y - fy, pz - fz)); float d010 = dot(g010, vec3(x - fx, y - cy, pz - fz)); float d110 = dot(g110, vec3(x - cx, y - cy, pz - fz)); float d001 = dot(g001, vec3(x - fx, y - fy, pz - cz)); float d101 = dot(g101, vec3(x - cx, y - fy, pz - cz)); float d011 = dot(g011, vec3(x - fx, y - cy, pz - cz)); float d111 = dot(g111, vec3(x - cx, y - cy, pz - cz)); float sx = quinticSmooth(x - fx); float sy = quinticSmooth(y - fy); float sz = quinticSmooth(pz - fz); float lx00 = mix(d000, d100, sx); float lx10 = mix(d010, d110, sx); float lx01 = mix(d001, d101, sx); float lx11 = mix(d011, d111, sx); float ly0 = mix(lx00, lx10, sy); float ly1 = mix(lx01, lx11, sy); return amplitude * mix(ly0, ly1, sz); } float auroraGlow(float t, vec2 shift) { vec2 uv = gl_FragCoord.xy / uResolution.y; uv += shift; float noiseVal = 0.0; float freq = uNoiseFreq; float amp = uNoiseAmp; vec2 samplePos = uv * uScale; for (float i = 0.0; i < 3.0; i += 1.0) { noiseVal += perlin3D(amp, freq, samplePos.x, samplePos.y, t); amp *= uOctaveDecay; freq *= 2.0; } float yBand = (uv.y - 0.5) * 5.0; return max(exp(-2.2 * abs(noiseVal + yBand * 0.45)), 0.0); } void main() { vec2 uv = gl_FragCoord.xy / uResolution.xy; float t = uSpeed * 0.4 * uTime; vec2 p = (gl_FragCoord.xy - 0.5 * uResolution.xy) / uResolution.y; float dist = length(p); float radius = 0.36; float edge = fwidth(dist) * 1.5; float mask = 1.0 - smoothstep(radius - edge, radius + edge, dist); if (mask <= 0.0) { fragColor = vec4(0.0); return; } vec2 shift = vec2(0.0); if (uEnableMouse) { shift = (uMouse - 0.5) * uMouseInfluence; } // 1. Dynamic Aurora Waves Field vec3 auroraCol = vec3(0.0); auroraCol += 0.95 * auroraGlow(t, shift) * cosineGradient(uv.x + uTime * uSpeed * 0.2 * uColorSpeed, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.3, 0.20, 0.20)) * uColor1; auroraCol += 0.95 * auroraGlow(t + uLayerOffset, shift) * cosineGradient(uv.x + uTime * uSpeed * 0.1 * uColorSpeed, vec3(0.5), vec3(0.5), vec3(2.0, 1.0, 0.0), vec3(0.5, 0.20, 0.25)) * uColor2; // 2. Full Sphere Ambient Fill (renders whole circular ball filled with dynamic aurora glow) float normDist = clamp(dist / radius, 0.0, 1.0); float centerVol = sqrt(max(1.0 - normDist * normDist, 0.0)); vec3 sphereBg = mix(uColor1, uColor2, sin(uv.x * 3.1415 + t * 0.5) * 0.5 + 0.5) * 0.45 * centerVol; vec3 col = sphereBg + auroraCol * uBrightness; // 3. Mild Frosted Glass Rim Reflection & Border Toggle (No dark shadow) if (uShowBorder) { float borderAngle = atan(p.y, p.x); vec3 glassTint = mix(uColor1, uColor2, sin(borderAngle * 2.0 + t * 1.2) * 0.5 + 0.5); vec3 glassHighlight = mix(vec3(1.0), glassTint * 1.2, 0.35); // Sleek mild frosted glass rim highlight along outer radius (0.91 - 0.99) float glassRim = sin(smoothstep(0.91, 0.99, normDist) * 3.14159); // Blend mild crystal glass rim highlight col = mix(col, glassHighlight, glassRim * 0.45); } // Soft spherical alpha falloff float alpha = (0.75 + 0.25 * centerVol) * mask; fragColor = vec4(col * alpha, alpha); } `; export interface AiAuroraBlobProps extends React.HTMLAttributes { /** Diameter of the AI Aurora Blob container in pixels (default: 260) */ size?: number; /** Primary aurora gradient color (default: "#10b981") */ color1?: string; /** Secondary aurora gradient color (default: "#6366f1") */ color2?: string; /** AI Assistant state mode: "listening" | "thinking" | "speaking" | "orbit" | "pulse" */ variant?: "listening" | "thinking" | "speaking" | "orbit" | "pulse"; /** Aurora wave speed multiplier (default: 0.6) */ speed?: number; /** Aurora wave scale (default: 1.5) */ scale?: number; /** Aurora brightness (default: 1.2) */ brightness?: number; /** Noise frequency (default: 2.5) */ noiseFrequency?: number; /** Noise amplitude (default: 1.0) */ noiseAmplitude?: number; /** Aurora band height position (default: 0.5) */ bandHeight?: number; /** Aurora band spread (default: 1.0) */ bandSpread?: number; /** Enable mouse interaction warping (default: true) */ enableMouseInteraction?: boolean; /** Mouse warp influence factor (default: 0.25) */ mouseInfluence?: number; /** Toggle outer glassmorphic border rim (default: true) */ showBorder?: boolean; /** Active voice listening state */ isListening?: boolean; /** Central mic click handler */ onMicClick?: () => void; /** Custom content inside center orb */ centerContent?: React.ReactNode; } export const AiAuroraBlob: React.FC = ({ size = 260, color1 = "#10b981", color2 = "#6366f1", variant = "listening", speed = 0.6, scale = 1.5, brightness = 1.2, noiseFrequency = 2.5, noiseAmplitude = 1.0, bandHeight = 0.5, bandSpread = 1.0, enableMouseInteraction = true, mouseInfluence = 0.25, showBorder = true, isListening = true, onMicClick, centerContent, className = "", style, onMouseEnter, onMouseLeave, ...props }) => { const containerRef = useRef(null); const [isHovered, setIsHovered] = useState(false); // Dynamic state variations based on AI Voice Assistant mode const variantConfig = React.useMemo(() => { switch (variant) { case "listening": return { speedMult: 1.3, brightMult: 1.1, freqMult: 1.1 }; case "thinking": return { speedMult: 1.9, brightMult: 1.3, freqMult: 1.8 }; case "speaking": return { speedMult: 2.3, brightMult: 1.4, freqMult: 1.4 }; case "orbit": return { speedMult: 0.8, brightMult: 1.0, freqMult: 0.9 }; case "pulse": return { speedMult: 1.1, brightMult: 1.2, freqMult: 1.0 }; default: return { speedMult: 1.0, brightMult: 1.0, freqMult: 1.0 }; } }, [variant]); const hoverSpeedMult = isHovered ? 2.4 : 1.0; const hoverBrightMult = isHovered ? 1.25 : 1.0; const propsRef = useRef({ speed: speed * variantConfig.speedMult * hoverSpeedMult, scale, brightness: brightness * variantConfig.brightMult * hoverBrightMult, color1, color2, noiseFrequency: noiseFrequency * variantConfig.freqMult, noiseAmplitude, bandHeight, bandSpread, enableMouseInteraction, mouseInfluence, showBorder, }); propsRef.current = { speed: speed * variantConfig.speedMult * hoverSpeedMult, scale, brightness: brightness * variantConfig.brightMult * hoverBrightMult, color1, color2, noiseFrequency: noiseFrequency * variantConfig.freqMult, noiseAmplitude, bandHeight, bandSpread, enableMouseInteraction, mouseInfluence, showBorder, }; useEffect(() => { const container = containerRef.current; if (!container) return; let renderer: Renderer | null = null; try { renderer = new Renderer({ alpha: true, premultipliedAlpha: true }); } catch (e) { console.warn("WebGL not supported for AiAuroraBlob", e); return; } const gl = renderer.gl; gl.clearColor(0, 0, 0, 0); gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.canvas.style.backgroundColor = "transparent"; let program: Program; let currentMouse = [0.5, 0.5]; let targetMouse = [0.5, 0.5]; function handleMouseMove(e: MouseEvent) { const rect = gl.canvas.getBoundingClientRect(); targetMouse = [ (e.clientX - rect.left) / (rect.width || 1), 1.0 - (e.clientY - rect.top) / (rect.height || 1), ]; } function handleMouseLeave() { targetMouse = [0.5, 0.5]; } function resize() { if (!container || !renderer) return; const w = container.offsetWidth || size; const h = container.offsetHeight || size; renderer.setSize(w, h); if (program) { program.uniforms.uResolution.value = [w, h, w / h]; } } const resizeObserver = new ResizeObserver(resize); resizeObserver.observe(container); resize(); const geometry = new Triangle(gl); program = new Program(gl, { vertex: vertexShader, fragment: fragmentShader, uniforms: { uTime: { value: 0 }, uResolution: { value: [container.offsetWidth || size, container.offsetHeight || size, 1.0] }, uSpeed: { value: propsRef.current.speed }, uScale: { value: propsRef.current.scale }, uBrightness: { value: propsRef.current.brightness }, uColor1: { value: hexToVec3(propsRef.current.color1) }, uColor2: { value: hexToVec3(propsRef.current.color2) }, uNoiseFreq: { value: propsRef.current.noiseFrequency }, uNoiseAmp: { value: propsRef.current.noiseAmplitude }, uBandHeight: { value: propsRef.current.bandHeight }, uBandSpread: { value: propsRef.current.bandSpread }, uOctaveDecay: { value: 0.1 }, uLayerOffset: { value: 0.5 }, uColorSpeed: { value: 1.0 }, uMouse: { value: new Float32Array([0.5, 0.5]) }, uMouseInfluence: { value: propsRef.current.mouseInfluence }, uEnableMouse: { value: propsRef.current.enableMouseInteraction }, uShowBorder: { value: propsRef.current.showBorder }, }, }); const mesh = new Mesh(gl, { geometry, program }); container.appendChild(gl.canvas); if (enableMouseInteraction) { gl.canvas.addEventListener("mousemove", handleMouseMove); gl.canvas.addEventListener("mouseleave", handleMouseLeave); } let animationFrameId: number; function update(time: number) { animationFrameId = requestAnimationFrame(update); if (!renderer) return; const current = propsRef.current; program.uniforms.uTime.value = time * 0.001; program.uniforms.uSpeed.value = current.speed; program.uniforms.uScale.value = current.scale; program.uniforms.uBrightness.value = current.brightness; program.uniforms.uColor1.value = hexToVec3(current.color1); program.uniforms.uColor2.value = hexToVec3(current.color2); program.uniforms.uNoiseFreq.value = current.noiseFrequency; program.uniforms.uNoiseAmp.value = current.noiseAmplitude; program.uniforms.uBandHeight.value = current.bandHeight; program.uniforms.uBandSpread.value = current.bandSpread; program.uniforms.uEnableMouse.value = current.enableMouseInteraction; program.uniforms.uMouseInfluence.value = current.mouseInfluence; program.uniforms.uShowBorder.value = current.showBorder; if (current.enableMouseInteraction) { currentMouse[0] += 0.08 * (targetMouse[0] - currentMouse[0]); currentMouse[1] += 0.08 * (targetMouse[1] - currentMouse[1]); program.uniforms.uMouse.value[0] = currentMouse[0]; program.uniforms.uMouse.value[1] = currentMouse[1]; } else { program.uniforms.uMouse.value[0] = 0.5; program.uniforms.uMouse.value[1] = 0.5; } renderer.render({ scene: mesh }); } animationFrameId = requestAnimationFrame(update); return () => { cancelAnimationFrame(animationFrameId); resizeObserver.disconnect(); if (enableMouseInteraction && gl.canvas) { gl.canvas.removeEventListener("mousemove", handleMouseMove); gl.canvas.removeEventListener("mouseleave", handleMouseLeave); } if (container && gl.canvas.parentNode === container) { container.removeChild(gl.canvas); } gl.getExtension("WEBGL_lose_context")?.loseContext(); }; }, [size]); return (
{ setIsHovered(true); onMouseEnter?.(e); }} onMouseLeave={(e) => { setIsHovered(false); onMouseLeave?.(e); }} className={`relative inline-flex items-center justify-center select-none overflow-hidden cursor-pointer group ${className}`} style={{ width: size, height: size, ...style }} {...props} > {/* ── Background WebGL Procedural Soft Aurora Waves ── */}
{/* ── Center Frosted Glassmorphic Voice Button ── */}
{centerContent ? ( centerContent ) : isListening ? (
{/* 1. LISTENING MODE: Dynamic Audio Equalizer Bounce */} {variant === "listening" && ( <> )} {/* 2. THINKING MODE: Floating Loading Wave Dots */} {variant === "thinking" && ( <> )} {/* 3. SPEAKING MODE: High Energy Rapid Speech Oscillation */} {variant === "speaking" && ( <> )} {/* 4. ORBIT MODE: Rotating 3-Dot Orbital Loop */} {variant === "orbit" && ( )} {/* 5. PULSE MODE: Synchronized Breathing Equalizer Pulse */} {variant === "pulse" && ( <> )}
) : ( )}
); }; export default AiAuroraBlob; ``` -------------------------------------------------- ## CATEGORY (FREE): Components (48) ### COMPONENT: animated-copy-button Category: Components Description: A highly interactive copy-to-clipboard button using Framer Motion with smooth success animations. URL: https://lightswind.com/components/animated-copy-button Import: import { AnimatedCopyButton } from "@/components/lightswind/animated-copy-button" Registry URL: https://lightswind.com/r/animated-copy-button.json Install Command: npx lightswind@latest add animated-copy-button Usage: ```tsx import { AnimatedCopyButton } from "@/components/lightswind/animated-copy-button"; export function AnimatedCopyButtonDemo() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Check, Copy } from "lucide-react"; import { cn } from "@/components/lib/utils"; interface AnimatedCopyButtonProps { /** The text that will be copied to the clipboard */ textToCopy: string; /** Optional classname for the button */ className?: string; /** Size of the button */ size?: "sm" | "md" | "lg"; /** Optional callback fired when copied */ onCopy?: () => void; } export function AnimatedCopyButton({ textToCopy, className, size = "md", onCopy, }: AnimatedCopyButtonProps) { const [isCopied, setIsCopied] = useState(false); const handleCopy = async () => { try { await navigator.clipboard.writeText(textToCopy); setIsCopied(true); if (onCopy) onCopy(); // Reset after 2 seconds setTimeout(() => { setIsCopied(false); }, 2000); } catch (err) { console.error("Failed to copy text: ", err); } }; const sizes = { sm: "h-8 w-8", md: "h-10 w-10", lg: "h-12 w-12", }; const iconSizes = { sm: "h-4 w-4", md: "h-5 w-5", lg: "h-6 w-6", }; return ( ); } ``` -------------------------------------------------- ### COMPONENT: animated-notification Category: Components Description: A professional animated notification center with smooth transitions, blur effects, and customizable styling. Displays notifications one-by-one with beautiful entrance and exit animations. URL: https://lightswind.com/components/animated-notification Import: import AnimatedNotification from '@/components/lightswind/animated-notification'; Registry URL: https://lightswind.com/r/animated-notification.json Install Command: npx lightswind@latest add animated-notification Usage: ```tsx console.log(notification)} /> ``` Source Code: ```tsx "use client"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { X } from "lucide-react"; import { cn } from "@/components/lib/utils"; export interface NotificationUser { avatarUrl?: string; name: string; initials?: string; color?: string; } export interface NotificationItem { id: string; user: NotificationUser; message: string; timestamp?: string; priority?: "low" | "medium" | "high"; type?: "info" | "success" | "warning" | "error"; fadingOut?: boolean; } export interface AnimatedNotificationProps { maxNotifications?: number; autoInterval?: number; autoGenerate?: boolean; notifications?: NotificationItem[]; customMessages?: string[]; animationDuration?: number; position?: "top-left" | "top-right" | "bottom-left" | "bottom-right" | "center"; width?: number; showAvatars?: boolean; showTimestamps?: boolean; className?: string; onNotificationClick?: (notification: NotificationItem) => void; onNotificationDismiss?: (notification: NotificationItem) => void; allowDismiss?: boolean; autoDismissTimeout?: number; userApiEndpoint?: string; variant?: "default" | "minimal" | "glass" | "bordered"; fixedUser?: NotificationUser; } const defaultMessages = [ "Just completed a task! ✅", "New feature deployed 🚀", "Check out our latest update 📱", "Server responded with 200 OK ✨", "Background job finished 🔄", "Data synced successfully! 💾", "User logged in successfully 👋", "Payment processed 💳", "Email sent successfully 📧", "Backup completed 🛡️", ]; // Lightweight native ID generator with zero external dependencies function generateId(): string { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID(); } return Math.random().toString(36).substring(2, 9) + Date.now().toString(36); } const Avatar: React.FC<{ user: NotificationUser; showAvatar: boolean }> = ({ user, showAvatar }) => { if (!showAvatar) return null; return (
{user.avatarUrl ? ( {`${user.name} ) : ( {user.initials || user.name.split(" ").map((n) => n[0]).join("").slice(0, 2).toUpperCase()} )}
); }; const Notification: React.FC<{ notification: NotificationItem; showAvatars: boolean; showTimestamps: boolean; variant: string; onDismiss?: () => void; onClick?: () => void; allowDismiss: boolean; }> = ({ notification, showAvatars, showTimestamps, variant, onDismiss, onClick, allowDismiss }) => { const getVariantStyles = () => { switch (variant) { case "minimal": return "bg-background/95 border border-border/50 backdrop-blur-xl"; case "glass": return "bg-background/30 backdrop-blur-2xl border border-white/20 dark:border-gray-800/20 shadow-2xl"; case "bordered": return "bg-card/95 border-2 border-primary/30 backdrop-blur-lg shadow-xl"; default: return "bg-background/30 backdrop-blur-2xl border border-white/20 shadow-2xl"; } }; const getPriorityStyles = () => { switch (notification.priority) { case "high": return "border-l-4 border-l-red-500 shadow-red-500/20 dark:border-l-red-500 dark:shadow-red-500/20"; case "medium": return "border-l-4 border-l-yellow-500 shadow-yellow-500/20 dark:border-l-yellow-500 dark:shadow-yellow-500/20"; case "low": return "border-l-4 border-l-blue-500 shadow-[0_4px_15px_color-mix(in_srgb,var(--primarylw)_20%,transparent)] dark:border-l-blue-500 dark:shadow-[0_4px_15px_color-mix(in_srgb,var(--primarylw)_20%,transparent)]"; default: return "border-l-4 border-l-primary/50 shadow-primary/20 dark:border-l-primary/50 dark:shadow-primary/20"; } }; return (

{notification.user.name}

{showTimestamps && notification.timestamp && ( {notification.timestamp} )}

{notification.message}

{allowDismiss && ( )}
); }; async function fetchRandomUser(apiEndpoint?: string): Promise { try { const endpoint = apiEndpoint || "https://randomuser.me/api/"; const res = await fetch(endpoint); const data = await res.json(); const user = data.results[0]; return { avatarUrl: user.picture?.large, name: `${user.name.first} ${user.name.last}`, color: `hsl(${Math.floor(Math.random() * 360)}, 70%, 80%)`, }; } catch { const names = ["John Doe", "Jane Smith", "Alex Johnson", "Sarah Wilson", "Mike Brown"]; const randomName = names[Math.floor(Math.random() * names.length)]; return { name: randomName, color: `hsl(${Math.floor(Math.random() * 360)}, 70%, 80%)`, }; } } function getRandomMessage(customMessages?: string[]) { const messages = customMessages || defaultMessages; return messages[Math.floor(Math.random() * messages.length)]; } async function generateNotification( customMessages?: string[], userApiEndpoint?: string, fixedUser?: NotificationUser ): Promise { const user = fixedUser || (await fetchRandomUser(userApiEndpoint)); return { id: generateId(), user, message: getRandomMessage(customMessages), timestamp: new Date().toLocaleTimeString(), priority: (["low", "medium", "high"] as const)[Math.floor(Math.random() * 3)], }; } export const AnimatedNotification: React.FC = ({ maxNotifications = 3, autoInterval = 3500, autoGenerate = true, notifications = [], customMessages, animationDuration = 400, position = "center", width = 320, showAvatars = true, showTimestamps = true, className, onNotificationClick, onNotificationDismiss, allowDismiss = true, autoDismissTimeout = 3000, userApiEndpoint, variant = "glass", fixedUser, }) => { const [notes, setNotes] = useState(notifications); const intervalRef = useRef(null); const dismissTimeouts = useRef>(new Map()); const isGenerating = useRef(false); // Clear a specific note's auto-dismiss timer const clearNoteTimeout = useCallback((id: string) => { const t = dismissTimeouts.current.get(id); if (t) { window.clearTimeout(t); dismissTimeouts.current.delete(id); } }, []); // Dismiss a notification smoothly const dismissNotification = useCallback( (id: string) => { clearNoteTimeout(id); setNotes((prev) => { const note = prev.find((n) => n.id === id); if (note && onNotificationDismiss) { onNotificationDismiss(note); } return prev.filter((n) => n.id !== id); }); }, [clearNoteTimeout, onNotificationDismiss] ); // Add a newly generated note const addGeneratedNote = useCallback(async () => { if (!autoGenerate || isGenerating.current) return; isGenerating.current = true; try { const newNote = await generateNotification(customMessages, userApiEndpoint, fixedUser); setNotes((prev) => { let updated = [...prev]; // Prune: if at or exceeding maxNotifications, dismiss oldest if (updated.length >= maxNotifications) { const removed = updated.shift(); if (removed) { clearNoteTimeout(removed.id); onNotificationDismiss?.(removed); } } updated.push(newNote); // Schedule auto-dismiss for new note if timeout > 0 if (autoDismissTimeout > 0) { const timeoutId = window.setTimeout(() => { dismissNotification(newNote.id); }, autoDismissTimeout); dismissTimeouts.current.set(newNote.id, timeoutId); } return updated; }); } finally { isGenerating.current = false; } }, [ autoGenerate, customMessages, userApiEndpoint, fixedUser, maxNotifications, autoDismissTimeout, clearNoteTimeout, onNotificationDismiss, dismissNotification, ]); // Start interval generator useEffect(() => { if (autoGenerate) { intervalRef.current = window.setInterval(() => { void addGeneratedNote(); }, autoInterval); const first = window.setTimeout(() => void addGeneratedNote(), 800); return () => { if (intervalRef.current) { window.clearInterval(intervalRef.current); intervalRef.current = null; } window.clearTimeout(first); }; } else { if (intervalRef.current) { window.clearInterval(intervalRef.current); intervalRef.current = null; } } }, [autoGenerate, autoInterval, addGeneratedNote]); // Sync external notifications prop useEffect(() => { if (notifications && notifications.length > 0) { dismissTimeouts.current.forEach((t) => window.clearTimeout(t)); dismissTimeouts.current.clear(); setNotes(notifications); if (autoDismissTimeout > 0) { notifications.forEach((n) => { const id = window.setTimeout(() => dismissNotification(n.id), autoDismissTimeout); dismissTimeouts.current.set(n.id, id); }); } } }, [notifications, autoDismissTimeout, dismissNotification]); // Cleanup all timers on unmount useEffect(() => { return () => { if (intervalRef.current) window.clearInterval(intervalRef.current); dismissTimeouts.current.forEach((t) => window.clearTimeout(t)); dismissTimeouts.current.clear(); }; }, []); const getPositionStyles = () => { switch (position) { case "top-left": return "fixed top-6 left-6 z-50"; case "top-right": return "fixed top-6 right-6 z-50"; case "bottom-left": return "fixed bottom-6 left-6 z-50"; case "bottom-right": return "fixed bottom-6 right-6 z-50"; default: return "flex items-center justify-center min-h-auto p-6"; } }; const animDurationSec = animationDuration / 1000; return (
{notes.map((note) => ( onNotificationClick?.(note)} onDismiss={() => dismissNotification(note.id)} /> ))}
); }; export default AnimatedNotification; ``` -------------------------------------------------- ### COMPONENT: code-hover-cards Category: Components Description: Interactive cards with dynamic character matrix effects on hover. Features customizable gradients, animations, and responsive layouts with professional styling and accessibility support. URL: https://lightswind.com/components/code-hover-cards Import: import ThreeDHoverGallery from "@/components/lightswind/3d-hover-gallery" Registry URL: https://lightswind.com/r/code-hover-cards.json Install Command: npx lightswind@latest add code-hover-cards Usage: ```tsx import CodeHoverCards from '@/components/lightswind/code-hover-cards'; import { Github, Code, Dices } from 'lucide-react'; // Basic usage with default cards // Custom cards with icons and links const cards = [ { id: '1', icon: Github, title: 'GitHub', href: 'https://github.com' }, { id: '2', icon: Code, title: 'Code Editor' }, { id: '3', icon: Dices, title: 'Games' }, ]; console.log('Clicked:', card)} /> // Advanced configuration handleCardClick(card)} onCardHover={(card) => handleCardHover(card)} /> ``` Source Code: ```tsx "use client"; import React, { useState, useRef } from 'react'; import { cn } from '@/components/lib/utils'; import { Github, Code, Dices, Terminal, Settings, Heart, Star, Zap, Trophy, Shield, } from 'lucide-react'; export interface CardData { id: string; icon: React.ComponentType; href?: string; title?: string; description?: string; } export interface CodeHoverCardsProps { cards?: CardData[]; className?: string; cardClassName?: string; maskRadius?: number; characterCount?: number; characterSet?: string; animationDuration?: number; borderRadius?: number; cardGap?: string; iconSize?: number; enableTouch?: boolean; columns?: 1 | 2 | 3 | 4; minHeight?: number; onCardClick?: (card: CardData) => void; onCardHover?: (card: CardData) => void; disabled?: boolean; showBorder?: boolean; theme?: 'normal' | 'dark'; // retained for fallback } const DEFAULT_CARDS: CardData[] = [ { id: '1', icon: Github, title: 'GitHub', description: 'Code repository' }, { id: '2', icon: Code, title: 'Code', description: 'Development tools' }, { id: '3', icon: Dices, title: 'Games', description: 'Interactive projects' }, ]; const DEFAULT_ICONS = [Github, Code, Dices, Terminal, Settings, Heart, Star, Zap, Trophy, Shield]; const CodeHoverCards: React.FC = ({ cards = DEFAULT_CARDS, className, cardClassName, maskRadius = 300, characterCount = 2000, characterSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', animationDuration = 0.5, borderRadius = 26, cardGap = '1rem', iconSize = 48, enableTouch = true, columns = 3, minHeight = 399, onCardClick, onCardHover, disabled = false, showBorder = true, theme = 'normal', }) => { const [mousePositions, setMousePositions] = useState<{ [key: string]: { x: number; y: number } }>({}); const [randomTexts, setRandomTexts] = useState<{ [key: string]: string }>({}); const cardRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const generateRandomString = (length: number): string => { return Array.from({ length }, () => characterSet[Math.floor(Math.random() * characterSet.length)]).join(''); }; const handleMouseMove = (e: React.MouseEvent, cardId: string) => { if (disabled) return; const card = cardRefs.current[cardId]; if (!card) return; const rect = card.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setMousePositions(prev => ({ ...prev, [cardId]: { x, y } })); setRandomTexts(prev => ({ ...prev, [cardId]: generateRandomString(characterCount) })); }; const handleTouchMove = (e: React.TouchEvent, cardId: string) => { if (disabled || !enableTouch) return; const card = cardRefs.current[cardId]; if (!card) return; const rect = card.getBoundingClientRect(); const touch = e.touches[0]; const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; setMousePositions(prev => ({ ...prev, [cardId]: { x, y } })); setRandomTexts(prev => ({ ...prev, [cardId]: generateRandomString(characterCount) })); }; const handleCardClick = (card: CardData) => { if (disabled) return; if (card.href) window.open(card.href, '_blank'); onCardClick?.(card); }; const handleCardHover = (card: CardData) => { if (disabled) return; onCardHover?.(card); }; const getColumnClass = () => { const columnMap = { 1: 'grid-cols-1', 2: 'grid-cols-1 md:grid-cols-2', 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4', }; return columnMap[columns]; }; return (
{cards.map((card) => { const IconComponent = card.icon; const position = mousePositions[card.id] || { x: 0, y: 0 }; const randomText = randomTexts[card.id] || ''; return (
{ cardRefs.current[card.id] = el; }} className={cn( 'relative w-full h-full flex items-center justify-center overflow-hidden cursor-pointer transition-all duration-200', 'hover:scale-105 active:scale-95', showBorder && 'border' )} style={{ borderRadius: borderRadius + 'px', minHeight: minHeight + 'px', aspectRatio: '1', }} onMouseMove={(e) => handleMouseMove(e, card.id)} onTouchMove={enableTouch ? (e) => handleTouchMove(e, card.id) : undefined} onClick={() => handleCardClick(card)} onMouseEnter={() => handleCardHover(card)} > {/* Icon */}
{/* Gradient overlay */}
{/* Character background */}
{randomText}
{/* Card info */} {(card.title || card.description) && (
{card.title && (

{card.title}

)} {card.description && (

{card.description}

)}
)}
); })}
); }; export default CodeHoverCards; ``` -------------------------------------------------- ### COMPONENT: count-up Category: Components Description: Animated counter that counts from zero to a target value with configurable effects. URL: https://lightswind.com/components/count-up Import: import { CountUp } from "@/components/lightswind/count-up" Registry URL: https://lightswind.com/r/count-up.json Install Command: npx lightswind@latest add count-up Usage: ```tsx import { CountUp } from "@/components/lightswind/count-up" export function CountUpExample() { return (

Basic Usage

With Prefix and Suffix

Custom Animation

Interactive

) } ``` Source Code: ```tsx "use client"; import React, { useState, useEffect, useRef } from "react"; import { motion, useMotionValue, useTransform, animate } from "framer-motion"; import { cn } from "@/components/lib/utils"; // Helper function to format the number const formatValue = (val: number, precision: number, sep: string): string => { return val .toFixed(precision) .replace(/\B(?=(\d{3})+(?!\d))/g, sep); }; export interface CountUpProps { value: number; duration?: number; decimals?: number; prefix?: string; suffix?: string; easing?: "linear" | "easeIn" | "easeOut" | "easeInOut"; separator?: string; interactive?: boolean; triggerOnView?: boolean; className?: string; numberClassName?: string; animationStyle?: "default" | "bounce" | "spring" | "gentle" | "energetic"; colorScheme?: "default" | "gradient" | "primary" | "secondary" | "custom"; customColor?: string; onAnimationComplete?: () => void; } const easingFunctions = { linear: [0, 0, 1, 1], easeIn: [0.42, 0, 1, 1], easeOut: [0, 0, 0.58, 1], easeInOut: [0.42, 0, 0.58, 1], }; const animationStyles = { default: { type: "tween" }, bounce: { type: "spring", bounce: 0.25 }, spring: { type: "spring", stiffness: 100, damping: 10 }, gentle: { type: "spring", stiffness: 60, damping: 15 }, energetic: { type: "spring", stiffness: 300, damping: 20 }, }; const colorSchemes = { default: "text-foreground", gradient: "bg-clip-text text-transparent bg-gradient-to-r from-primarylw to-purple-600", primary: "text-primary", secondary: "text-secondary", custom: "", }; export function CountUp({ value, duration = 2, decimals = 0, prefix = "", suffix = "", easing = "easeOut", separator = ",", interactive = false, triggerOnView = true, className, numberClassName, animationStyle = "default", colorScheme = "default", customColor, onAnimationComplete, }: CountUpProps) { const [hasAnimated, setHasAnimated] = useState(false); const containerRef = useRef(null); const count = useMotionValue(0); const rounded = useTransform(count, (latest) => formatValue(latest, decimals, separator) ); useEffect(() => { let controls: { stop: () => void } | null = null; const startAnimation = () => { controls = animate(count.get(), value, { ...(animationStyles[animationStyle] as any), ease: easingFunctions[easing], duration: animationStyle === "default" ? duration : undefined, onUpdate: (latest) => count.set(latest), onComplete: () => { setHasAnimated(true); onAnimationComplete?.(); }, }); }; if (!triggerOnView || hasAnimated) { startAnimation(); return () => controls?.stop(); } const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting && !hasAnimated) { startAnimation(); } }, { threshold: 0.1 } ); if (containerRef.current) { observer.observe(containerRef.current); } return () => { controls?.stop(); observer.disconnect(); }; }, [value, duration, easing, animationStyle, triggerOnView, hasAnimated, onAnimationComplete, count]); const hasCustomTextColor = className?.includes("text-") || numberClassName?.includes("text-"); const colorClass = colorScheme === "custom" && customColor ? "" : colorScheme !== "default" ? colorSchemes[colorScheme] : hasCustomTextColor ? "" : colorSchemes.default; const getHoverAnimation = () => { if (!interactive) return {}; return { whileHover: { scale: 1.05, filter: "brightness(1.1)", transition: { duration: 0.2 }, }, whileTap: { scale: 0.95, filter: "brightness(0.95)", transition: { duration: 0.1 }, }, }; }; return (
{prefix && {prefix}} {rounded} {suffix && {suffix}}
); } export default CountUp; ``` -------------------------------------------------- ### COMPONENT: dock Category: Components Description: A customizable macOS-style dock component with proximity-based magnification, badges, frosted glass, separators, and full layout control. URL: https://lightswind.com/components/dock Import: import Dock from '@/components/lightswind/dock'; Registry URL: https://lightswind.com/r/dock.json Install Command: npx lightswind@latest add dock Usage: ```tsx import Dock from '@/components/lightswind/dock'; import { Home, Settings, Mail, Calendar, Music, User } from 'lucide-react'; const dockItems = [ { icon: , label: 'Home', onClick: () => console.log('Home') }, { icon: , label: 'Mail', onClick: () => console.log('Mail'), badgeCount: 4 }, { icon: , label: 'Calendar', onClick: () => console.log('Calendar') }, { icon: , label: 'Music', onClick: () => console.log('Music') }, { icon: , label: 'Settings', onClick: () => console.log('Settings') }, { icon: , label: 'Profile', onClick: () => console.log('Profile'), badgeCount: 1 }, ]; ``` Source Code: ```tsx "use client"; import { motion, useMotionValue, useSpring, useTransform, AnimatePresence, MotionValue, } from "framer-motion"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/components/lib/utils"; function useDockItemSize( mouseX: MotionValue, baseItemSize: number, magnification: number, distance: number, ref: React.RefObject, spring: { mass: number; stiffness: number; damping: number } ) { const mouseDistance = useTransform(mouseX, (val) => { if (typeof val !== "number" || isNaN(val)) return 0; const rect = ref.current?.getBoundingClientRect() ?? { x: 0, width: baseItemSize, }; return val - rect.x - baseItemSize / 2; }); const targetSize = useTransform( mouseDistance, [-distance, 0, distance], [baseItemSize, magnification, baseItemSize] ); return useSpring(targetSize, spring); } interface DockItemProps { icon: React.ReactNode; label: string; onClick: () => void; mouseX: MotionValue; baseItemSize: number; magnification: number; distance: number; spring: { mass: number; stiffness: number; damping: number }; badgeCount?: number; badgeColor?: string; itemBackground?: string; itemBorderColor?: string; borderRadius?: string; hideLabels?: boolean; labelPosition?: "top" | "bottom"; labelBackground?: string; labelTextColor?: string; } function DockItem({ icon, label, onClick, mouseX, baseItemSize, magnification, distance, spring, badgeCount, badgeColor = "bg-red-500", itemBackground, itemBorderColor, borderRadius, hideLabels = false, labelPosition = "top", labelBackground, labelTextColor, }: DockItemProps) { const ref = useRef(null); const isHovered = useMotionValue(0); const size = useDockItemSize(mouseX, baseItemSize, magnification, distance, ref, spring); const [showLabel, setShowLabel] = useState(false); useEffect(() => { const unsubscribe = isHovered.on("change", (value) => setShowLabel(value === 1) ); return () => unsubscribe(); }, [isHovered]); const labelOffsetStyle = labelPosition === "bottom" ? { top: "calc(100% + 8px)", bottom: "auto" } : { bottom: "calc(100% + 8px)", top: "auto" }; return ( isHovered.set(1)} onHoverEnd={() => isHovered.set(0)} onFocus={() => isHovered.set(1)} onBlur={() => isHovered.set(0)} onClick={onClick} className={cn( "relative inline-flex items-center justify-center shadow-md cursor-pointer", borderRadius ?? "rounded-full", itemBackground ?? "bg-background", itemBorderColor ? `border-2 ${itemBorderColor}` : "" )} tabIndex={0} role="button" aria-haspopup="true" >
{icon}
{/* Badge */} {badgeCount !== undefined && badgeCount > 0 && ( {badgeCount > 99 ? "99+" : badgeCount} )} {/* Tooltip Label */} {!hideLabels && ( {showLabel && ( {label} )} )}
); } interface DockItemData { icon: React.ReactNode; label: string; onClick: () => void; badgeCount?: number; } interface DockProps { /** Items to render inside the dock. Each needs an icon, label, and optional onClick + badgeCount. */ items: DockItemData[]; /** Extra CSS classes for the dock panel container. */ className?: string; /** Framer Motion spring physics for all animations. */ spring?: { mass: number; stiffness: number; damping: number }; /** Maximum size in px items magnify to on hover. @default 70 */ magnification?: number; /** Distance in px over which magnification spreads to neighbours. @default 200 */ distance?: number; /** Dock panel height in px when idle. @default 64 */ panelHeight?: number; /** Dock panel max height in px when expanded. @default 256 */ dockHeight?: number; /** Base (idle) size of each item in px. @default 50 */ baseItemSize?: number; /** Dock position on screen. Only "bottom" is currently animated. @default "bottom" */ position?: "bottom" | "top"; /** Gap between dock items. Tailwind gap class e.g. "gap-2", "gap-6". @default "gap-4" */ gap?: string; /** Background color for individual item icons. Tailwind class or CSS value. */ itemBackground?: string; /** Border color class for each item (e.g. "border-zinc-300"). */ itemBorderColor?: string; /** Border radius override for items. Tailwind class (e.g. "rounded-xl"). @default "rounded-full" */ borderRadius?: string; /** Color for the notification badge dot. Tailwind bg class. @default "bg-red-500" */ badgeColor?: string; /** Whether to hide all hover labels. @default false */ hideLabels?: boolean; /** Position of the hover label relative to the icon. @default "top" */ labelPosition?: "top" | "bottom"; /** Background for the hover label tooltip. Tailwind class. @default "bg-[#060606]" */ labelBackground?: string; /** Text color for the hover label tooltip. Tailwind class. @default "text-white" */ labelTextColor?: string; /** If true, renders a frosted glass blur panel behind the dock. @default false */ blurBackground?: boolean; /** If true, wraps the dock with the blocks multi-layer border concept frame. @default true */ multiBorder?: boolean; /** Show a visual separator line. @default false */ showSeparator?: boolean; /** Index after which the separator is placed (0-based). */ separatorIndex?: number; } export default function Dock({ items, className = "", spring = { mass: 0.1, stiffness: 150, damping: 12 }, magnification = 70, distance = 200, panelHeight = 64, dockHeight = 256, baseItemSize = 50, gap = "gap-4", itemBackground, itemBorderColor, borderRadius, badgeColor = "bg-red-500", hideLabels = false, labelPosition = "top", labelBackground, labelTextColor, blurBackground = false, multiBorder = true, showSeparator = false, separatorIndex, }: DockProps) { const mouseX = useMotionValue(Infinity); const isHovered = useMotionValue(0); const maxHeight = useMemo( () => Math.max(dockHeight, magnification + magnification / 2 + 4), [magnification, dockHeight] ); const animatedHeight = useSpring( useTransform(isHovered, [0, 1], [panelHeight, maxHeight]), spring ); return ( { isHovered.set(1); mouseX.set(pageX); }} onMouseLeave={() => { isHovered.set(0); mouseX.set(Infinity); }} className={cn( "absolute bottom-2 left-1/2 -translate-x-1/2 transform flex items-center justify-center w-fit transition-all duration-300", multiBorder ? "p-[3px] rounded-[24px] sm:rounded-[28px] border border-zinc-200/80 dark:border-zinc-800/80 bg-zinc-100/80 dark:bg-zinc-900/80 shadow-lg backdrop-blur-xl" : "", className )} style={{ height: panelHeight }} role="toolbar" aria-label="Application dock" >
{items.map((item, index) => ( {showSeparator && separatorIndex === index && (
)} ))}
); } export { Dock }; ``` -------------------------------------------------- ### COMPONENT: drag-order-list Category: Components Description: A draggable, reorderable list built using React, Motion One, and Tailwind CSS. Items can be rearranged vertically via drag-and-drop gestures, with smooth animation and visual feedback. URL: https://lightswind.com/components/drag-order-list Import: import { DragOrderList } from '@/components/lightswind/drag-order-list'; Registry URL: https://lightswind.com/r/drag-order-list.json Install Command: npx lightswind@latest add drag-order-list Usage: ```tsx import { DragOrderList } from '@/components/lightswind/drag-order-list'; const items = [ { id: 1, title: "Task One", subtitle: "This is the first task", date: "2025-07-29", link: "https://example.com/task-1" }, { id: 2, title: "Task Two", subtitle: "This is the second task", date: "2025-07-28" }, ]; console.log("Reordered items:", newOrder)} /> ``` Source Code: ```tsx "use client"; import React, { useEffect } from "react"; import { useMotionValue, Reorder, useDragControls, motion, animate, DragControls, } from "framer-motion"; import { GripVertical } from "lucide-react"; export interface DragItem { id: number; title: string; subtitle: string; date: string; link?: string; } interface DragOrderListProps { items: DragItem[]; onReorder?: (items: DragItem[]) => void; } export function DragOrderList({ items, onReorder }: DragOrderListProps) { const [list, setList] = React.useState(items); useEffect(() => { if (onReorder) onReorder(list); }, [list]); return ( {list.map((item) => ( ))} ); } function DragOrderItem({ item }: { item: DragItem }) { const y = useMotionValue(0); const boxShadow = useRaisedShadow(y); const dragControls = useDragControls(); return (

{item.title}

{item.subtitle}

{item.date} {item.link && ( View details about {item.title} )}
); } function ReorderHandle({ dragControls }: { dragControls: DragControls }) { return ( { e.preventDefault(); dragControls.start(e); }} className="cursor-grab active:cursor-grabbing p-2 text-muted-foreground" > ); } const inactiveShadow = "0px 0px 0px rgba(0,0,0,0.8)"; function useRaisedShadow(value: ReturnType) { const boxShadow = useMotionValue(inactiveShadow); useEffect(() => { let isActive = false; return value.on("change", (latest) => { const wasActive = isActive; if (latest !== 0) { isActive = true; if (isActive !== wasActive) { animate(boxShadow, "5px 5px 15px rgba(0,0,0,0.15)"); } } else { isActive = false; if (isActive !== wasActive) { animate(boxShadow, inactiveShadow); } } }); }, [value, boxShadow]); return boxShadow; } ``` -------------------------------------------------- ### COMPONENT: draggable-reorder-list Category: Components Description: A smooth drag-to-reorder list using Framer Motion's Reorder API with spring physics, staggered entry, and optional item removal. URL: https://lightswind.com/components/draggable-reorder-list Import: import { DraggableReorderList, ReorderItem } from "@/components/lightswind/draggable-reorder-list" Registry URL: https://lightswind.com/r/draggable-reorder-list.json Install Command: npx lightswind@latest add draggable-reorder-list Usage: ```tsx import { DraggableReorderList, ReorderItem } from "@/components/lightswind/draggable-reorder-list"; const items: ReorderItem[] = [ { id: "1", label: "First Item", description: "Description here" }, { id: "2", label: "Second Item", description: "Another description" }, ]; export function ReorderDemo() { return ( console.log(newOrder)} /> ); } ``` Source Code: ```tsx "use client"; import React, { useState } from "react"; import { motion, AnimatePresence, Reorder, useDragControls, } from "framer-motion"; import { GripVertical, X, Plus } from "lucide-react"; import { cn } from "@/components/lib/utils"; export interface ReorderItem { id: string; label: string; description?: string; icon?: React.ReactNode; } interface DraggableReorderListProps { /** Initial items */ items: ReorderItem[]; /** Callback with new order when reordered */ onReorder?: (items: ReorderItem[]) => void; /** Allow removing items */ removable?: boolean; /** Additional classes */ className?: string; } function Item({ item, onRemove, removable, }: { item: ReorderItem; onRemove: (id: string) => void; removable: boolean; }) { const dragControls = useDragControls(); return ( e.preventDefault()} className={cn( "flex items-center gap-3 rounded-xl border bg-background px-4 py-3", "shadow-sm hover:shadow-md transition-shadow cursor-default select-none" )} > {/* Drag Handle */} dragControls.start(e)} className="flex-shrink-0 cursor-grab active:cursor-grabbing touch-none text-muted-foreground/40 hover:text-muted-foreground transition-colors" whileHover={{ scale: 1.1 }} > {/* Icon */} {item.icon && (
{item.icon}
)} {/* Content */}

{item.label}

{item.description && (

{item.description}

)}
{/* Remove Button */} {removable && ( onRemove(item.id)} aria-label={`Remove ${item.label}`} className="flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10 transition-colors focus:outline-none" whileHover={{ scale: 1.15 }} whileTap={{ scale: 0.9 }} > )}
); } export function DraggableReorderList({ items: initialItems, onReorder, removable = true, className, }: DraggableReorderListProps) { const [items, setItems] = useState(initialItems); const handleReorder = (newOrder: ReorderItem[]) => { setItems(newOrder); onReorder?.(newOrder); }; const handleRemove = (id: string) => { const next = items.filter((item) => item.id !== id); setItems(next); onReorder?.(next); }; return (
{items.map((item) => ( ))} {items.length === 0 && (

All items removed

)}
); } ``` -------------------------------------------------- ### COMPONENT: electro-border Category: Components Description: A dynamic animated border with an electric, distortion-based effect. Supports optional glow and aura layers for an energized or minimal look. URL: https://lightswind.com/components/electro-border Import: import ElectroBorder from "@/components/lightswind/electro-border"; Registry URL: https://lightswind.com/r/electro-border.json Install Command: npx lightswind@latest add electro-border Usage: ```tsx // 1. Default Electric Border
Electric Energy!
// 2. Only Electric Border (no glow or aura)
Pure Electric Border
// 3. Electric Border with Glow Only
Glow Only
// 4. Electric Border with Aura Only
Aura Only
// 5. Custom speed, distortion, and color
Custom Lightning Frame
``` Source Code: ```tsx "use client"; import React, { useRef, useLayoutEffect, useEffect, useId, CSSProperties, PropsWithChildren, useCallback, } from "react"; /* ----------------------------- 🔧 Utility: HEX → RGBA ------------------------------ */ const toRGBA = (color: string, alpha = 1): string => { if (!color) return `rgba(0, 255, 252, ${alpha})`; if (color.startsWith("#")) { const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color; if (hex.length === 7) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } } if (typeof window === "undefined") return color; try { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) return color; ctx.fillStyle = color; const computed = ctx.fillStyle; if (computed.startsWith("rgba")) { return computed.replace(/[\d.]+\)$/g, `${alpha})`); } if (computed.startsWith("rgb")) { return computed.replace("rgb", "rgba").replace(")", `, ${alpha})`); } if (computed.startsWith("#")) { const r = parseInt(computed.slice(1, 3), 16); const g = parseInt(computed.slice(3, 5), 16); const b = parseInt(computed.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } } catch { return color; } return color; }; /* ----------------------------- ⚙️ Props Definition ------------------------------ */ export interface ElectroBorderProps extends PropsWithChildren { /** Border electric neon color (e.g., #00fffc, #ff7700, #ff007f) */ borderColor?: string; /** Custom card background color / gradient (optional, auto theme-adaptive by default) */ cardBackground?: string; /** Border thickness in px (default 2) */ borderWidth?: number; /** Animation distortion intensity (default 1) */ distortion?: number; /** Animation speed multiplier (default 1) */ animationSpeed?: number; /** Border radius (default "24px") */ radius?: string | number; /** 🔘 Enable outer glow effects (default true) */ glow?: boolean; /** 🔘 Enable aura background reflection (default true) */ aura?: boolean; /** 🔘 Enable glossy light overlays (default true) */ overlay?: boolean; /** 🔘 Master toggle for all decorative layers */ effects?: boolean; /** Glow blur intensity */ glowBlur?: number; /** Number of octaves for the turbulence noise filter (default 2 for optimal 60fps performance) */ numOctaves?: number; className?: string; style?: CSSProperties; } /* ----------------------------- ⚡ ElectroBorder Component ------------------------------ */ export const ElectroBorder: React.FC = ({ children, borderColor = "#00fffc", cardBackground, borderWidth = 2, distortion = 1, animationSpeed = 1, radius = "24px", glow = true, aura = true, overlay = true, effects = true, glowBlur = 32, numOctaves = 2, className, style, }) => { const rootRef = useRef(null); const svgRef = useRef(null); const mainCardRef = useRef(null); const rawId = useId(); const filterId = `turbulent-displace-${rawId.replace(/[^a-zA-Z0-9_-]/g, "")}`; const parsedRadius = typeof radius === "number" ? `${radius}px` : radius; /* ----------------------------- 🔄 Filter Animation Control ------------------------------ */ const updateFilter = useCallback((wOverride?: number, hOverride?: number) => { const svg = svgRef.current; const root = rootRef.current; if (!svg || !root) return; let w = wOverride; let h = hOverride; if (!w || !h) { const rect = root.getBoundingClientRect(); w = Math.max(100, Math.round(rect.width || 350)); h = Math.max(100, Math.round(rect.height || 500)); } const dy1 = svg.querySelector(`#${filterId}-dy-anim-1`); const dy2 = svg.querySelector(`#${filterId}-dy-anim-2`); const dx1 = svg.querySelector(`#${filterId}-dx-anim-1`); const dx2 = svg.querySelector(`#${filterId}-dx-anim-2`); if (dy1) dy1.setAttribute("values", `${h}; 0`); if (dy2) dy2.setAttribute("values", `0; -${h}`); if (dx1) dx1.setAttribute("values", `${w}; 0`); if (dx2) dx2.setAttribute("values", `0; -${w}`); const duration = Math.max(0.1, 6 / Math.max(0.1, animationSpeed)); const animations = [dy1, dy2, dx1, dx2]; animations.forEach((anim) => { if (anim) { anim.setAttribute("dur", `${duration}s`); if ((anim as any).beginElement) { try { (anim as any).beginElement(); } catch {} } } }); const disp = svg.querySelector("feDisplacementMap"); if (disp) { disp.setAttribute("scale", `${30 * distortion}`); } }, [animationSpeed, distortion, filterId]); useLayoutEffect(() => { updateFilter(); const root = rootRef.current; if (!root) return; const observer = new ResizeObserver((entries) => { if (entries[0]) { const { width, height } = entries[0].contentRect; if (width && height) { updateFilter(Math.round(width), Math.round(height)); } } }); observer.observe(root); return () => observer.disconnect(); }, [updateFilter]); useEffect(() => { updateFilter(); }, [updateFilter]); // Viewport-aware performance optimization: pause SMIL animations when off-screen useEffect(() => { const root = rootRef.current; const svg = svgRef.current; if (!root || !svg || typeof IntersectionObserver === "undefined") return; const observer = new IntersectionObserver( ([entry]) => { if (entry && entry.isIntersecting) { try { if (typeof svg.unpauseAnimations === "function") { svg.unpauseAnimations(); } } catch {} } else { try { if (typeof svg.pauseAnimations === "function") { svg.pauseAnimations(); } } catch {} } }, { rootMargin: "250px" } ); observer.observe(root); return () => observer.disconnect(); }, []); const gradientColor = toRGBA(borderColor, 0.25); const electricLight = toRGBA(borderColor, 0.9); const electricDim = toRGBA(borderColor, 0.5); return (
{/* Theme-Adaptive Card Background Layer (No hard shadow) */}
{/* Subtle Gradient Color Tint */} {!cardBackground && (
)} {/* SVG Filter Definition */} {/* Inner Border Layers */}
{/* Outer guide border */}
{/* Main Turbulent Animated Electric Border */}
{/* Glow Layer 1 */} {effects && glow && (
)} {/* Glow Layer 2 */} {effects && glow && (
)}
{/* Glossy Electric Overlay */} {effects && overlay && (
)} {/* Ambient Background Aura Glow (True Electric Theme Color) */} {effects && aura && (
)} {/* Content Slot */}
{children}
); }; export default ElectroBorder; ``` -------------------------------------------------- ### COMPONENT: expandable-search-bar Category: Components Description: An elegant, animated search bar that expands gracefully on interaction and features clear/command micro-interactions. URL: https://lightswind.com/components/expandable-search-bar Import: import { ExpandableSearchBar } from "@/components/lightswind/expandable-search-bar" Registry URL: https://lightswind.com/r/expandable-search-bar.json Install Command: npx lightswind@latest add expandable-search-bar Usage: ```tsx import { ExpandableSearchBar } from "@/components/lightswind/expandable-search-bar"; export function SearchBarDemo() { return (
); } ``` Source Code: ```tsx "use client"; import React, { useState, useRef, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Search, X, Command } from "lucide-react"; import { cn } from "@/components/lib/utils"; interface ExpandableSearchBarProps { /** Optional placeholder text */ placeholder?: string; /** Optional onChange handler */ onChange?: (value: string) => void; /** Optional onSubmit handler */ onSubmit?: (value: string) => void; /** Additional CSS classes */ className?: string; /** The maximum expanded width (Tailwind class or absolute value like "300px") */ expandedWidth?: string | number; } export function ExpandableSearchBar({ placeholder = "Search...", onChange, onSubmit, className, expandedWidth = "18rem", // 288px (w-72) }: ExpandableSearchBarProps) { const [isExpanded, setIsExpanded] = useState(false); const [value, setValue] = useState(""); const inputRef = useRef(null); const containerRef = useRef(null); // Focus input when expanded useEffect(() => { if (isExpanded && inputRef.current) { inputRef.current.focus(); } }, [isExpanded]); // Handle outside click to collapse if empty useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) && value === "" ) { setIsExpanded(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [value]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (onSubmit) onSubmit(value); }; const handleClear = () => { setValue(""); if (onChange) onChange(""); inputRef.current?.focus(); }; return (
!isExpanded && setIsExpanded(true)} > { setValue(e.target.value); if (onChange) onChange(e.target.value); }} placeholder={placeholder} className="h-full w-full border-none bg-transparent pl-10 pr-10 text-sm outline-none placeholder:text-muted-foreground/60 focus:border-transparent focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:outline-none focus:ring-offset-0 focus-visible:ring-offset-0" style={{ pointerEvents: isExpanded ? "auto" : "none", opacity: isExpanded ? 1 : 0, boxShadow: "none", }} tabIndex={isExpanded ? 0 : -1} /> {isExpanded && value === "" && (
K
)} {isExpanded && value !== "" && ( )}
); } ``` -------------------------------------------------- ### COMPONENT: expandable-speed-dial Category: Components Description: A functional speed dial that expands into multiple floating action buttons with staggered physics animations. URL: https://lightswind.com/components/expandable-speed-dial Import: import { ExpandableSpeedDial, SpeedDialAction } from "@/components/lightswind/expandable-speed-dial" Registry URL: https://lightswind.com/r/expandable-speed-dial.json Install Command: npx lightswind@latest add expandable-speed-dial Usage: ```tsx import { ExpandableSpeedDial, SpeedDialAction } from "@/components/lightswind/expandable-speed-dial"; import { FileEdit, Share2, Download, Printer } from "lucide-react"; export function SpeedDialDemo() { const actions: SpeedDialAction[] = [ { icon: , label: "Edit", onClick: () => console.log("Edit") }, { icon: , label: "Share", onClick: () => console.log("Share") }, ]; return (
); } ``` Source Code: ```tsx "use client"; import React, { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Plus } from "lucide-react"; import { cn } from "@/components/lib/utils"; export interface SpeedDialAction { icon: React.ReactNode; label: string; onClick: () => void; } interface ExpandableSpeedDialProps { /** The actions to display when expanded */ actions: SpeedDialAction[]; /** The direction to expand the speed dial */ direction?: "up" | "down" | "left" | "right"; /** Optional classname for the container */ className?: string; /** Size of the main button */ size?: "sm" | "md" | "lg"; } export function ExpandableSpeedDial({ actions, direction = "up", className, size = "md", }: ExpandableSpeedDialProps) { const [isOpen, setIsOpen] = useState(false); const toggleOpen = () => setIsOpen(!isOpen); const sizes = { sm: "h-10 w-10", md: "h-12 w-12", lg: "h-14 w-14", }; const actionSizes = { sm: "h-8 w-8", md: "h-10 w-10", lg: "h-12 w-12", }; const getDirectionClasses = () => { switch (direction) { case "up": return "bottom-full mb-3 flex-col-reverse left-1/2 -translate-x-1/2"; case "down": return "top-full mt-3 flex-col left-1/2 -translate-x-1/2"; case "left": return "right-full mr-3 flex-row-reverse top-1/2 -translate-y-1/2"; case "right": return "left-full ml-3 flex-row top-1/2 -translate-y-1/2"; default: return "bottom-full mb-3 flex-col-reverse left-1/2 -translate-x-1/2"; } }; const getMotionVariants = (index: number) => { const delay = index * 0.05; const distance = 15; let x = 0; let y = 0; switch (direction) { case "up": y = distance; break; case "down": y = -distance; break; case "left": x = distance; break; case "right": x = -distance; break; } return { hidden: { opacity: 0, scale: 0.5, x, y }, visible: { opacity: 1, scale: 1, x: 0, y: 0, transition: { type: "spring" as const, stiffness: 300, damping: 20, delay } }, exit: { opacity: 0, scale: 0.5, x, y, transition: { duration: 0.2, delay: (actions.length - 1 - index) * 0.05 } } }; }; return (
{isOpen && (
{actions.map((action, index) => ( { action.onClick(); setIsOpen(false); }} className={cn( "relative flex items-center justify-center rounded-full bg-background shadow-md border hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", actionSizes[size] )} title={action.label} aria-label={action.label} whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} > {action.icon} {/* Tooltip for horizontal/vertical depending on direction */} {(direction === "up" || direction === "down") && ( {action.label} )} {(direction === "left" || direction === "right") && ( {action.label} )} ))}
)}
); } ``` -------------------------------------------------- ### COMPONENT: glass-folder Category: Components Description: A 3D glassmorphic folder component with layered depth and interactive rotation effects on hover. Designed using React and Tailwind CSS, it visually mimics a stack of translucent folder sheets, making it ideal for showcasing documents, portfolios, or feature categories in a stylish and futuristic way. URL: https://lightswind.com/components/glass-folder Import: import GlassFolder from '@/components/lightswind/glass-folder'; Registry URL: https://lightswind.com/r/glass-folder.json Install Command: npx lightswind@latest add glass-folder Usage: ```tsx import GlassFolder from '@/components/lightswind/glass-folder'; import { FileText } from 'lucide-react'; ; ``` Source Code: ```tsx "use client"; import React from "react"; import { cn } from "@/components/lib/utils"; // Optional: For class merging utility type GlassFolderProps = { icon?: React.ReactNode; className?: string; }; const GlassFolder: React.FC = ({ icon, className }) => { return (
{/* Top tab */}
{/* Folder layers */}
{/* Front folder layer with icon */}
{icon}
); }; export default GlassFolder; ``` -------------------------------------------------- ### COMPONENT: globe Category: Components Description: A fully customizable and interactive 3D globe built with the `cobe` library. Supports theming, lighting, rotation, mouse interactions, and custom markers. Ideal for showcasing global presence, user locations, or ambient visual flair. URL: https://lightswind.com/components/globe Import: import Globe from '@/components/lightswind/globe'; Registry URL: https://lightswind.com/r/globe.json Install Command: npx lightswind@latest add globe Usage: ```tsx import Globe from '@/components/lightswind/globe'; // Basic usage with default values // Custom appearance and behavior ``` Source Code: ```tsx "use client"; import React, { useEffect, useRef } from "react"; import createGlobe from "cobe"; import { cn } from "@/components/lib/utils"; // Utility function to convert a hex color string to a normalized RGB array [0-1, 0-1, 0-1] const hexToRgbNormalized = (hex: string): [number, number, number] => { let r = 0, g = 0, b = 0; const cleanHex = hex.startsWith("#") ? hex.slice(1) : hex; if (cleanHex.length === 3) { r = parseInt(cleanHex[0] + cleanHex[0], 16); g = parseInt(cleanHex[1] + cleanHex[1], 16); b = parseInt(cleanHex[2] + cleanHex[2], 16); } else if (cleanHex.length === 6) { r = parseInt(cleanHex.substring(0, 2), 16); g = parseInt(cleanHex.substring(2, 4), 16); b = parseInt(cleanHex.substring(4, 6), 16); } else { return [0.4, 0.65, 1]; } return [r / 255, g / 255, b / 255]; }; export interface GlobeMarker { location: [number, number]; size: number; } export interface GlobeProps { className?: string; theta?: number; phi?: number; dark?: number; scale?: number; diffuse?: number; mapSamples?: number; mapBrightness?: number; baseColor?: [number, number, number] | string; markerColor?: [number, number, number] | string; glowColor?: [number, number, number] | string; markers?: GlobeMarker[]; /** Enable mouse wheel and pinch zoom */ enableZoom?: boolean; /** Minimum zoom scale */ minScale?: number; /** Maximum zoom scale */ maxScale?: number; /** Zoom sensitivity multiplier */ zoomSensitivity?: number; /** Enable auto rotation */ autoRotate?: boolean; /** Auto rotation speed */ autoRotateSpeed?: number; } const Globe: React.FC = ({ className, theta = 0.25, phi = 0, dark = 0, scale = 1.1, diffuse = 1.2, mapSamples = 24000, mapBrightness = 10, baseColor = "#ffffff", markerColor = "#ff3b30", glowColor = "#ffffff", markers = [], enableZoom = true, minScale = 0.4, maxScale = 3.5, zoomSensitivity = 0.002, autoRotate = true, autoRotateSpeed = 0.003, }) => { const canvasRef = useRef(null); const globeRef = useRef(null); // Interaction refs const phiRef = useRef(phi); const thetaRef = useRef(theta); const targetScaleRef = useRef(scale); const currentScaleRef = useRef(scale); const isDragging = useRef(false); const lastMouseX = useRef(0); const lastMouseY = useRef(0); // Synchronize initial prop scale useEffect(() => { targetScaleRef.current = scale; }, [scale]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; // Normalize color props const resolvedBaseColor: [number, number, number] = typeof baseColor === "string" ? hexToRgbNormalized(baseColor) : baseColor; const resolvedMarkerColor: [number, number, number] = typeof markerColor === "string" ? hexToRgbNormalized(markerColor) : markerColor; const resolvedGlowColor: [number, number, number] = typeof glowColor === "string" ? hexToRgbNormalized(glowColor) : glowColor; const initGlobe = () => { if (globeRef.current) { globeRef.current.destroy(); globeRef.current = null; } const rect = canvas.getBoundingClientRect(); const width = Math.max(100, Math.round(rect.width || 600)); const height = Math.max(100, Math.round(rect.height || 500)); const dpr = Math.min(window.devicePixelRatio || 1, 2); const internalWidth = Math.round(width * dpr); const internalHeight = Math.round(height * dpr); canvas.width = internalWidth; canvas.height = internalHeight; globeRef.current = createGlobe(canvas, { devicePixelRatio: dpr, width: internalWidth, height: internalHeight, phi: phiRef.current, theta: thetaRef.current, dark: dark, scale: currentScaleRef.current, diffuse: diffuse, mapSamples: mapSamples, mapBrightness: mapBrightness, baseColor: resolvedBaseColor, markerColor: resolvedMarkerColor, glowColor: resolvedGlowColor, opacity: 1, offset: [0, 0], markers: markers, onRender: (state: Record) => { // Smooth zoom interpolation (lerp) for high-fps fluid zooming currentScaleRef.current += (targetScaleRef.current - currentScaleRef.current) * 0.12; if (!isDragging.current && autoRotate) { phiRef.current += autoRotateSpeed; } state.phi = phiRef.current; state.theta = thetaRef.current; state.scale = currentScaleRef.current; state.width = internalWidth; state.height = internalHeight; }, }); }; // --- Mouse Drag Interaction Handlers --- const onMouseDown = (e: MouseEvent) => { isDragging.current = true; lastMouseX.current = e.clientX; lastMouseY.current = e.clientY; canvas.style.cursor = "grabbing"; }; const onMouseMove = (e: MouseEvent) => { if (isDragging.current) { const deltaX = e.clientX - lastMouseX.current; const deltaY = e.clientY - lastMouseY.current; const rotationSpeed = 0.005; phiRef.current += deltaX * rotationSpeed; thetaRef.current = Math.max( -Math.PI / 2, Math.min(Math.PI / 2, thetaRef.current - deltaY * rotationSpeed) ); lastMouseX.current = e.clientX; lastMouseY.current = e.clientY; } }; const onMouseUp = () => { isDragging.current = false; canvas.style.cursor = "grab"; }; const onMouseLeave = () => { if (isDragging.current) { isDragging.current = false; canvas.style.cursor = "grab"; } }; // --- Mouse Wheel Zoom Interaction Handler --- const onWheel = (e: WheelEvent) => { if (!enableZoom) return; // Allow natural page scrolling; only intercept wheel when user is explicitly zooming with Ctrl/Cmd or dragging if (!e.ctrlKey && !e.metaKey && !isDragging.current) { return; } e.preventDefault(); const zoomDelta = -e.deltaY * zoomSensitivity; const nextScale = targetScaleRef.current + zoomDelta; targetScaleRef.current = Math.max(minScale, Math.min(maxScale, nextScale)); }; // --- Touch Interaction (Drag + Pinch to Zoom) --- let touchDistance = 0; let initialTouchScale = 1; const onTouchStart = (e: TouchEvent) => { if (e.touches.length === 1) { isDragging.current = true; lastMouseX.current = e.touches[0].clientX; lastMouseY.current = e.touches[0].clientY; } else if (e.touches.length === 2 && enableZoom) { isDragging.current = false; const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; touchDistance = Math.hypot(dx, dy); initialTouchScale = targetScaleRef.current; } }; const onTouchMove = (e: TouchEvent) => { if (e.touches.length === 1 && isDragging.current) { const deltaX = e.touches[0].clientX - lastMouseX.current; const deltaY = e.touches[0].clientY - lastMouseY.current; const rotationSpeed = 0.005; phiRef.current += deltaX * rotationSpeed; thetaRef.current = Math.max( -Math.PI / 2, Math.min(Math.PI / 2, thetaRef.current - deltaY * rotationSpeed) ); lastMouseX.current = e.touches[0].clientX; lastMouseY.current = e.touches[0].clientY; } else if (e.touches.length === 2 && enableZoom && touchDistance > 0) { e.preventDefault(); const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const dist = Math.hypot(dx, dy); const factor = dist / touchDistance; const nextScale = initialTouchScale * factor; targetScaleRef.current = Math.max(minScale, Math.min(maxScale, nextScale)); } }; const onTouchEnd = () => { isDragging.current = false; touchDistance = 0; }; let isVisible = true; const observer = typeof IntersectionObserver !== "undefined" ? new IntersectionObserver( ([entry]) => { const visible = entry && entry.isIntersecting; isVisible = visible; if (visible) { if (!globeRef.current) { initGlobe(); } } else { if (globeRef.current) { globeRef.current.destroy(); globeRef.current = null; } } }, { rootMargin: "150px" } ) : null; if (observer) { observer.observe(canvas); } else { initGlobe(); } // Attach interaction listeners canvas.addEventListener("mousedown", onMouseDown); canvas.addEventListener("mousemove", onMouseMove); canvas.addEventListener("mouseup", onMouseUp); canvas.addEventListener("mouseleave", onMouseLeave); canvas.addEventListener("wheel", onWheel, { passive: false }); canvas.addEventListener("touchstart", onTouchStart, { passive: true }); canvas.addEventListener("touchmove", onTouchMove, { passive: false }); canvas.addEventListener("touchend", onTouchEnd, { passive: true }); const handleResize = () => { if (isVisible) { initGlobe(); } }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); if (observer) { observer.disconnect(); } if (canvas) { canvas.removeEventListener("mousedown", onMouseDown); canvas.removeEventListener("mousemove", onMouseMove); canvas.removeEventListener("mouseup", onMouseUp); canvas.removeEventListener("mouseleave", onMouseLeave); canvas.removeEventListener("wheel", onWheel); canvas.removeEventListener("touchstart", onTouchStart); canvas.removeEventListener("touchmove", onTouchMove); canvas.removeEventListener("touchend", onTouchEnd); } if (globeRef.current) { globeRef.current.destroy(); globeRef.current = null; } }; }, [ theta, dark, diffuse, mapSamples, mapBrightness, baseColor, markerColor, glowColor, enableZoom, minScale, maxScale, zoomSensitivity, autoRotate, autoRotateSpeed, markers, ]); return (
); }; export default Globe; ``` -------------------------------------------------- ### COMPONENT: grain-carousel Category: Components Description: An interactive 3D lenticular dual-image carousel with animated holographic gradient foil, micro film grain, and elevation tilt effects. URL: https://lightswind.com/components/grain-carousel Import: import GrainCarousel, { GrainCarouselItem } from "@/components/lightswind/grain-carousel"; Registry URL: https://lightswind.com/r/grain-carousel.json Install Command: npx lightswind@latest add grain-carousel Usage: ```tsx import GrainCarousel, { GrainCarouselItem } from "@/components/lightswind/grain-carousel"; const items: GrainCarouselItem[] = [ { title: "Apex Structure", subtitle: "Parametric Glass Facade", badge: "Architecture", imageA: "https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=600&q=80", imageB: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=600&q=80", accentColor: "#00F5FF", }, { title: "Modern Pavilion", subtitle: "Minimalist Natural Wood", badge: "Concept", imageA: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=600&q=80", imageB: "https://images.unsplash.com/photo-1511818966892-d7d671e672a2?auto=format&fit=crop&w=600&q=80", accentColor: "#FF9FFC", }, ]; export default function Example() { return ( ); } ``` Source Code: ```tsx "use client"; import React, { useState, useRef, useEffect, useCallback, useId, CSSProperties, forwardRef, useImperativeHandle, } from "react"; import { cn } from "@/components/lib/utils"; import { ChevronLeft, ChevronRight, Sparkles } from "lucide-react"; export type GrainFoilVariant = | "holographic" | "cosmic-cyan" | "neon-sakura" | "solar-plasma" | "cyber-emerald" | "ultra-violet" | "monochrome"; export interface GrainCarouselItem { id?: string | number; title?: string; subtitle?: string; category?: string; imageA: string; imageB?: string; badge?: string; href?: string; accentColor?: string; onClick?: () => void; } export interface GrainCarouselProps { /** List of carousel slide items with dual lenticular images and meta info */ items?: GrainCarouselItem[]; /** Initially active center card index (default: 2 or middle) */ defaultIndex?: number; /** Controlled active index */ activeIndex?: number; /** Callback fired when center slide changes */ onIndexChange?: (index: number, item: GrainCarouselItem) => void; /** Width of each card in pixels (default: 270) */ cardWidth?: number; /** Aspect ratio for cards (default: "3 / 4") */ aspectRatio?: string; /** Spacing between cards in pixels (default: 24) */ gap?: number; /** 3D perspective depth in pixels (default: 1200) */ perspective?: number; /** Z-axis lift elevation in px when card is hovered (default: 36) */ lift?: number; /** Maximum 3D rotation tilt angle in degrees (default: 16) */ maxTilt?: number; /** Aesthetic gradient foil color palette */ foilVariant?: GrainFoilVariant; /** Intensity of animated gradient film grain overlay (0.0 to 1.0, default: 0.35) */ grainAmount?: number; /** Number of vertical lenticular optical refraction strips (default: 56) */ lenticularStrips?: number; /** Scale factor for inactive background cards (0.5 to 1.0, default: 0.90) */ inactiveScale?: number; /** Brightness dim factor for inactive background cards (0.1 to 1.0, default: 0.55) */ inactiveDim?: number; /** Whether to show lenticular optical lens ribs (default: true) */ showRibs?: boolean; /** Whether to show animated holographic foil shimmer (default: true) */ showFoil?: boolean; /** Whether to show film grain overlay (default: true) */ showGrain?: boolean; /** Enable navigation arrows (default: true) */ showArrows?: boolean; /** Enable bottom segmented progress dots (default: true) */ showDots?: boolean; /** Enable automatic slide cycling (default: false) */ autoplay?: boolean; /** Autoplay transition interval in ms (default: 4000) */ autoplayInterval?: number; /** Pause autoplay when mouse enters carousel (default: true) */ pauseOnHover?: boolean; /** Border radius for cards (default: "16px") */ radius?: string | number; /** Optional container class name */ className?: string; /** Optional container inline style */ style?: CSSProperties; } export interface GrainCarouselHandle { next: () => void; prev: () => void; goTo: (index: number) => void; getIndex: () => number; } const DEFAULT_CAROUSEL_ITEMS: GrainCarouselItem[] = [ { id: 1, title: "Apex Structure", subtitle: "Parametric Glass Facade", badge: "Architecture", imageA: "https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=600&q=80", imageB: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=600&q=80", accentColor: "#00F5FF", }, { id: 2, title: "Modern Pavilion", subtitle: "Minimalist Natural Wood", badge: "Concept", imageA: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=600&q=80", imageB: "https://images.unsplash.com/photo-1511818966892-d7d671e672a2?auto=format&fit=crop&w=600&q=80", accentColor: "#FF9FFC", }, { id: 3, title: "Villa Solarium", subtitle: "Infinite Horizon Pool", badge: "Luxury", imageA: "https://images.unsplash.com/photo-1518780664697-55e3ad937233?auto=format&fit=crop&w=600&q=80", imageB: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=600&q=80", accentColor: "#FF9900", }, { id: 4, title: "Glass Skyline", subtitle: "Urban Geometric Tower", badge: "Metropolis", imageA: "https://images.unsplash.com/photo-1486325212027-8081e485255e?auto=format&fit=crop&w=600&q=80", imageB: "https://images.unsplash.com/photo-1570129477492-45c003edd2be?auto=format&fit=crop&w=600&q=80", accentColor: "#00FF88", }, { id: 5, title: "Monolith Center", subtitle: "Brutalist Concrete Curve", badge: "Exhibition", imageA: "https://images.unsplash.com/photo-1479839672679-a46483c0e7c8?auto=format&fit=crop&w=600&q=80", imageB: "https://images.unsplash.com/photo-1507652313519-d4e9174996dd?auto=format&fit=crop&w=600&q=80", accentColor: "#A855F7", }, ]; const FOIL_GRADIENTS: Record = { holographic: "linear-gradient(115deg, transparent 0%, rgba(255, 0, 128, 0.4) 25%, rgba(0, 255, 255, 0.5) 50%, rgba(255, 255, 0, 0.4) 75%, transparent 100%)", "cosmic-cyan": "linear-gradient(115deg, transparent 0%, rgba(0, 150, 255, 0.4) 25%, rgba(0, 245, 255, 0.6) 50%, rgba(120, 255, 214, 0.4) 75%, transparent 100%)", "neon-sakura": "linear-gradient(115deg, transparent 0%, rgba(255, 70, 150, 0.4) 25%, rgba(255, 160, 250, 0.6) 50%, rgba(180, 100, 255, 0.4) 75%, transparent 100%)", "solar-plasma": "linear-gradient(115deg, transparent 0%, rgba(255, 60, 0, 0.4) 25%, rgba(255, 160, 0, 0.6) 50%, rgba(255, 220, 50, 0.4) 75%, transparent 100%)", "cyber-emerald": "linear-gradient(115deg, transparent 0%, rgba(0, 200, 100, 0.4) 25%, rgba(0, 255, 140, 0.6) 50%, rgba(0, 230, 255, 0.4) 75%, transparent 100%)", "ultra-violet": "linear-gradient(115deg, transparent 0%, rgba(130, 0, 255, 0.4) 25%, rgba(190, 80, 255, 0.6) 50%, rgba(255, 70, 180, 0.4) 75%, transparent 100%)", monochrome: "linear-gradient(115deg, transparent 0%, rgba(255, 255, 255, 0.15) 30%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.15) 70%, transparent 100%)", }; export const GrainCarousel = forwardRef(({ items = DEFAULT_CAROUSEL_ITEMS, defaultIndex, activeIndex: controlledIndex, onIndexChange, cardWidth = 270, aspectRatio = "3 / 4", gap = 24, perspective = 1200, lift = 36, maxTilt = 16, foilVariant = "cosmic-cyan", grainAmount = 0.35, lenticularStrips = 56, inactiveScale = 0.90, inactiveDim = 0.55, showRibs = true, showFoil = true, showGrain = true, showArrows = true, showDots = true, autoplay = false, autoplayInterval = 4000, pauseOnHover = true, radius = "16px", className, style, }, ref) => { const rawDefault = defaultIndex !== undefined ? defaultIndex : Math.min(2, Math.max(0, Math.floor(items.length / 2))); const [internalIndex, setInternalIndex] = useState(rawDefault); const currentIndex = controlledIndex !== undefined ? controlledIndex : internalIndex; const [trackOffset, setTrackOffset] = useState(0); const [isHovered, setIsHovered] = useState(false); const [cardInteractions, setCardInteractions] = useState< Record >({}); const containerRef = useRef(null); const trackRef = useRef(null); const isDraggingRef = useRef(false); const dragStartXRef = useRef(0); const filterId = useId().replace(/[:]/g, ""); const parsedRadius = typeof radius === "number" ? `${radius}px` : radius; const changeIndex = useCallback( (newIndex: number) => { const clamped = Math.max(0, Math.min(newIndex, items.length - 1)); if (controlledIndex === undefined) { setInternalIndex(clamped); } if (onIndexChange && items[clamped]) { onIndexChange(clamped, items[clamped]); } }, [controlledIndex, items, onIndexChange] ); useImperativeHandle(ref, () => ({ next: () => changeIndex(currentIndex + 1), prev: () => changeIndex(currentIndex - 1), goTo: (idx) => changeIndex(idx), getIndex: () => currentIndex, })); // Update track position for active slide centering const updateTrackPosition = useCallback(() => { if (!containerRef.current) return; const containerWidth = containerRef.current.offsetWidth; const centerOffset = containerWidth / 2 - cardWidth / 2; const targetTranslate = centerOffset - currentIndex * (cardWidth + gap); setTrackOffset(targetTranslate); }, [currentIndex, cardWidth, gap]); useEffect(() => { updateTrackPosition(); const ro = new ResizeObserver(() => updateTrackPosition()); if (containerRef.current) ro.observe(containerRef.current); return () => ro.disconnect(); }, [updateTrackPosition]); // Autoplay Timer useEffect(() => { if (!autoplay || (pauseOnHover && isHovered) || items.length <= 1) return; const timer = setInterval(() => { setInternalIndex((prev) => (prev >= items.length - 1 ? 0 : prev + 1)); }, autoplayInterval); return () => clearInterval(timer); }, [autoplay, autoplayInterval, pauseOnHover, isHovered, items.length]); // Keyboard Navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowLeft") changeIndex(currentIndex - 1); if (e.key === "ArrowRight") changeIndex(currentIndex + 1); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [currentIndex, changeIndex]); // Mouse Interaction for 3D Tilt and Lenticular Transition const handleCardMouseMove = (e: React.MouseEvent, index: number) => { const card = e.currentTarget; const rect = card.getBoundingClientRect(); const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); const tiltY = (x - 0.5) * (maxTilt * 2); const tiltX = (0.5 - y) * (maxTilt * 1.5); const progress = x; setCardInteractions((prev) => ({ ...prev, [index]: { tiltX, tiltY, progress, foilX: x * 100, foilY: y * 100, }, })); }; const handleCardMouseLeave = (index: number) => { setCardInteractions((prev) => ({ ...prev, [index]: { tiltX: 0, tiltY: 0, progress: 0, foilX: 50, foilY: 50 }, })); }; // Drag / Swipe Handlers const handleMouseDown = (e: React.MouseEvent) => { isDraggingRef.current = true; dragStartXRef.current = e.clientX; }; const handleTouchStart = (e: React.TouchEvent) => { isDraggingRef.current = true; dragStartXRef.current = e.touches[0].clientX; }; const handleDragMove = (clientX: number) => { if (!isDraggingRef.current) return; const deltaX = clientX - dragStartXRef.current; if (Math.abs(deltaX) > 45) { if (deltaX > 0 && currentIndex > 0) { changeIndex(currentIndex - 1); } else if (deltaX < 0 && currentIndex < items.length - 1) { changeIndex(currentIndex + 1); } isDraggingRef.current = false; } }; const handleMouseUp = () => { isDraggingRef.current = false; }; return (
setIsHovered(true)} onMouseLeave={() => { setIsHovered(false); isDraggingRef.current = false; }} > {/* SVG Procedural Grain Noise Filter definition */} {/* 3D Viewport Wrapper */}
handleDragMove(e.clientX)} onMouseUp={handleMouseUp} onTouchStart={handleTouchStart} onTouchMove={(e) => handleDragMove(e.touches[0].clientX)} onTouchEnd={handleMouseUp} > {/* 3D Carousel Track */}
{items.map((item, index) => { const isActive = index === currentIndex; const interaction = cardInteractions[index] || { tiltX: 0, tiltY: 0, progress: 0, foilX: 50, foilY: 50, }; const hasDualImage = Boolean(item.imageB); const flipProgress = hasDualImage ? interaction.progress : 0; const foilOpacity = Math.sin(interaction.progress * Math.PI) * 0.75; return (
{ if (!isActive) changeIndex(index); if (item.onClick) item.onClick(); }} onMouseMove={(e) => handleCardMouseMove(e, index)} onMouseLeave={() => handleCardMouseLeave(index)} className={cn( "relative shrink-0 rounded-2xl cursor-pointer transition-all duration-500 ease-[cubic-bezier(0.2,0.8,0.2,1)] group", isActive ? "z-20 shadow-2xl ring-1 ring-white/20" : "z-10 shadow-lg hover:brightness-75" )} style={{ width: `${cardWidth}px`, aspectRatio, borderRadius: parsedRadius, transform: `scale(${isActive ? 1.0 : inactiveScale})`, filter: isActive ? "brightness(1)" : `brightness(${inactiveDim})`, transformStyle: "preserve-3d", }} > {/* 3D Hover Tilt & Lift Container */}
{/* Primary Image Face (Image A) */} {item.title {/* Secondary Lenticular Image Face (Image B) */} {hasDualImage && ( {item.title )} {/* Lenticular Lens Ribs Optical Refraction Overlay */} {showRibs && (
)} {/* Animated Holographic Foil Shimmer */} {showFoil && (
)} {/* Micro Film Grain Texture Overlay */} {showGrain && (
)} {/* Bottom Vignette Scrim */}
{/* Card Content Overlay */}
{item.badge && (
{item.badge}
)} {item.title && (

{item.title}

)} {item.subtitle && (

{item.subtitle}

)}
); })}
{/* Carousel Controls & Segmented Dots Rail */} {(showArrows || showDots) && (
{/* Previous Button */} {showArrows && ( )} {/* Progress Dots Rail */} {showDots && (
{items.map((_, i) => (
)} {/* Next Button */} {showArrows && ( )}
)}
); }); GrainCarousel.displayName = "GrainCarousel"; export default GrainCarousel; ``` -------------------------------------------------- ### COMPONENT: world-map Category: Components Description: A high-performance interactive SVG dotted world map featuring accurate lat/long coordinates, live multi-timezone beacon pings, curved connection arcs with animated particles, and rich dark & light mode styling. URL: https://lightswind.com/components/world-map Import: import WorldMap, { DEFAULT_MARKERS, DEFAULT_ARCS } from "@/components/lightswind/world-map"; Registry URL: https://lightswind.com/r/world-map.json Install Command: npx lightswind@latest add world-map Usage: ```tsx import WorldMap, { DEFAULT_MARKERS, DEFAULT_ARCS } from "@/components/lightswind/world-map"; // Basic usage with default markers and connection arcs
``` Source Code: ```tsx "use client"; import React, { useState, useEffect, useMemo } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { MapPin, Clock } from "lucide-react"; import { cn } from "@/components/lib/utils"; export interface MapMarker { id?: string; lat: number; lng: number; label?: string; country?: string; timeZone?: string; ping?: string; size?: number; color?: string; pulse?: boolean; data?: any; } export interface MapArc { id?: string; start: { lat: number; lng: number }; end: { lat: number; lng: number }; color?: string; strokeWidth?: number; dashed?: boolean; } export interface WorldMapProps extends React.SVGProps { width?: number; height?: number; dotRadius?: number; dotColor?: string; markerColor?: string; pulseColor?: string; markers?: MapMarker[]; arcs?: MapArc[]; pulse?: boolean; stagger?: boolean; enableTooltips?: boolean; showTimezones?: boolean; interactive?: boolean; className?: string; onMarkerClick?: (marker: MapMarker) => void; renderMarkerOverlay?: (args: { marker: MapMarker; x: number; y: number; r: number; }) => React.ReactNode; } // Convert Latitude / Longitude (WGS84) to SVG Map Coordinates (Equirectangular / Miller hybrid projection) export function latLngToXY(lat: number, lng: number, width: number = 800, height: number = 400) { // Longitude: -180 to +180 -> 0 to width const x = ((lng + 180) / 360) * width; // Latitude: +90 to -90 -> 0 to height (Miller-adjusted projection for balanced continents) const latRad = (lat * Math.PI) / 180; const mercN = Math.log(Math.tan(Math.PI / 4 + (latRad * 0.85) / 2)); const y = height / 2 - (width * mercN) / (2 * Math.PI); // Clamp boundaries safely const clampedX = Math.max(0, Math.min(width, x)); const clampedY = Math.max(0, Math.min(height, y)); return { x: clampedX, y: clampedY }; } // High-accuracy continent landmass sample grid points [lat, lng] const WORLD_LANDMASS_SAMPLES: [number, number][] = [ // North America - Alaska & Canada [71, -156], [68, -165], [64, -162], [65, -147], [61, -150], [60, -162], [58, -135], [69, -133], [65, -120], [62, -114], [67, -118], [63, -104], [60, -108], [64, -96], [58, -94], [55, -100], [53, -113], [54, -125], [51, -120], [49, -123], [52, -106], [50, -97], [53, -85], [58, -78], [55, -67], [52, -75], [48, -89], [47, -79], [46, -71], [47, -65], [45, -63], [53, -60], // USA (Lower 48) [48, -122], [46, -124], [44, -121], [42, -123], [39, -123], [37, -122], [34, -119], [32, -117], [47, -114], [44, -114], [40, -111], [37, -112], [34, -112], [32, -111], [48, -100], [45, -100], [41, -102], [37, -100], [33, -102], [30, -103], [28, -100], [47, -92], [43, -93], [39, -94], [35, -92], [32, -92], [30, -90], [44, -85], [41, -86], [38, -85], [34, -84], [30, -84], [27, -81], [25, -80], [43, -76], [40, -74], [37, -77], [35, -78], [32, -80], // Mexico & Central America [30, -110], [27, -108], [24, -105], [26, -100], [22, -101], [19, -99], [20, -90], [17, -93], [15, -90], [14, -87], [12, -85], [9, -83], [8, -80], // Caribbean [22, -79], [18, -72], [18, -66], // South America [10, -73], [7, -73], [4, -73], [1, -78], [-2, -79], [-5, -80], [-8, -78], [-12, -77], [-15, -75], [6, -62], [3, -60], [0, -50], [5, -53], [2, -66], [-2, -60], [-5, -63], [-8, -63], [-3, -44], [-6, -37], [-8, -35], [-12, -38], [-15, -44], [-12, -49], [-16, -68], [-19, -65], [-22, -65], [-23, -46], [-22, -43], [-25, -49], [-25, -57], [-28, -70], [-33, -71], [-38, -73], [-42, -73], [-46, -74], [-51, -73], [-29, -60], [-34, -58], [-38, -62], [-42, -64], [-46, -67], [-50, -68], [-54, -68], // Greenland & Iceland [76, -42], [72, -40], [70, -50], [65, -45], [64, -18], // Europe - UK & Ireland [58, -4], [55, -3], [52, -1], [51, 0], [53, -8], // Europe - Scandinavia [69, 25], [65, 22], [61, 25], [64, 14], [60, 11], [59, 17], [56, 13], // Europe - Western & Central [53, 5], [51, 4], [48, 2], [45, 0], [43, 3], [43, -3], [40, -4], [37, -5], [39, -9], [52, 10], [49, 11], [46, 9], [43, 12], [41, 14], [38, 15], [37, 14], [53, 19], [50, 19], [46, 17], [44, 21], [40, 22], [38, 23], // Europe - Eastern & Russia [58, 30], [55, 37], [52, 33], [48, 35], [45, 34], [46, 40], [65, 41], [60, 50], [55, 52], [52, 50], [48, 45], [68, 55], [64, 65], [60, 70], [56, 68], [52, 71], [67, 78], [63, 85], [59, 88], [55, 83], [52, 85], [68, 100], [63, 105], [58, 102], [54, 100], [67, 120], [62, 125], [57, 120], [53, 118], [66, 140], [62, 145], [58, 138], [54, 130], [64, 160], [60, 162], [56, 160], [52, 157], // Africa - North [36, 3], [34, 10], [32, 20], [31, 30], [31, -8], [28, -10], [25, 0], [25, 12], [26, 25], [26, 32], [21, -13], [20, -1], [20, 11], [20, 22], [21, 31], // Africa - West & Central [15, -16], [12, -8], [10, 0], [12, 14], [13, 25], [12, 37], [10, 42], [5, -3], [6, 3], [8, 10], [4, 19], [4, 28], [5, 36], [0, 10], [0, 20], [0, 29], [0, 38], // Africa - South [-5, 13], [-4, 22], [-4, 33], [-5, 39], [-11, 15], [-12, 26], [-11, 37], [-18, 15], [-18, 25], [-19, 34], [-19, 46], [-25, 17], [-24, 26], [-25, 33], [-30, 19], [-29, 27], [-30, 31], [-33, 22], [-34, 26], // Middle East [37, 36], [34, 44], [31, 47], [29, 40], [27, 45], [24, 45], [24, 54], [20, 56], [15, 48], [36, 50], [32, 54], [28, 56], // Central & South Asia [42, 60], [40, 68], [44, 75], [38, 73], [35, 68], [33, 70], [30, 68], [27, 65], [32, 76], [28, 77], [24, 75], [22, 80], [22, 88], [18, 74], [16, 80], [13, 77], [10, 78], [8, 80], // East Asia - China & Mongolia [48, 87], [44, 87], [41, 85], [47, 103], [44, 105], [40, 96], [40, 110], [42, 118], [45, 125], [36, 82], [35, 93], [35, 104], [36, 114], [37, 120], [39, 125], [30, 85], [30, 96], [30, 104], [31, 114], [31, 121], [25, 92], [24, 101], [25, 110], [24, 118], [22, 114], // Japan & Korea [38, 127], [35, 128], [43, 142], [38, 140], [35, 136], [33, 130], // Southeast Asia [19, 100], [18, 106], [14, 101], [15, 108], [11, 106], [6, 101], [3, 102], [1, 104], [16, 121], [13, 123], [9, 125], [1, 114], [-2, 115], [-2, 121], [-7, 110], [-8, 115], // Australia & New Zealand [-13, 131], [-15, 142], [-18, 123], [-21, 134], [-21, 148], [-25, 115], [-24, 126], [-25, 136], [-24, 151], [-30, 118], [-29, 129], [-31, 138], [-29, 153], [-34, 118], [-33, 136], [-35, 143], [-37, 145], [-34, 150], [-42, 146], [-37, 175], [-41, 174], [-45, 169] ]; export const DEFAULT_MARKERS: MapMarker[] = [ { id: "sf", lat: 37.7749, lng: -122.4194, label: "San Francisco", country: "US", timeZone: "America/Los_Angeles", ping: "12ms", size: 3, pulse: true }, { id: "nyc", lat: 40.7128, lng: -74.006, label: "New York", country: "US", timeZone: "America/New_York", ping: "18ms", size: 3, pulse: true }, { id: "lon", lat: 51.5074, lng: -0.1278, label: "London", country: "UK", timeZone: "Europe/London", ping: "8ms", size: 3, pulse: true }, { id: "ber", lat: 52.52, lng: 13.405, label: "Berlin", country: "DE", timeZone: "Europe/Berlin", ping: "14ms", size: 3, pulse: true }, { id: "dxb", lat: 25.2048, lng: 55.2708, label: "Dubai", country: "AE", timeZone: "Asia/Dubai", ping: "28ms", size: 3, pulse: true }, { id: "blr", lat: 12.9716, lng: 77.5946, label: "Bengaluru", country: "IN", timeZone: "Asia/Kolkata", ping: "19ms", size: 3, pulse: true }, { id: "sg", lat: 1.3521, lng: 103.8198, label: "Singapore", country: "SG", timeZone: "Asia/Singapore", ping: "15ms", size: 3, pulse: true }, { id: "tyo", lat: 35.6762, lng: 139.6503, label: "Tokyo", country: "JP", timeZone: "Asia/Tokyo", ping: "22ms", size: 3, pulse: true }, { id: "syd", lat: -33.8688, lng: 151.2093, label: "Sydney", country: "AU", timeZone: "Australia/Sydney", ping: "38ms", size: 3, pulse: true }, { id: "sp", lat: -23.5505, lng: -46.6333, label: "São Paulo", country: "BR", timeZone: "America/Sao_Paulo", ping: "45ms", size: 3, pulse: true }, ]; export const DEFAULT_ARCS: MapArc[] = [ { start: { lat: 37.7749, lng: -122.4194 }, end: { lat: 51.5074, lng: -0.1278 } }, // SF to London { start: { lat: 51.5074, lng: -0.1278 }, end: { lat: 12.9716, lng: 77.5946 } }, // London to Bengaluru { start: { lat: 12.9716, lng: 77.5946 }, end: { lat: 35.6762, lng: 139.6503 } }, // Bengaluru to Tokyo { start: { lat: 35.6762, lng: 139.6503 }, end: { lat: -33.8688, lng: 151.2093 } },// Tokyo to Sydney ]; export function WorldMap({ width = 900, height = 450, dotRadius = 1.8, dotColor, markerColor = "#3B82F6", pulseColor, markers = DEFAULT_MARKERS, arcs = DEFAULT_ARCS, pulse = true, stagger = true, enableTooltips = true, showTimezones = false, interactive = true, className, onMarkerClick, renderMarkerOverlay, style, ...svgProps }: WorldMapProps) { const [hoveredMarker, setHoveredMarker] = useState(null); const [now, setNow] = useState(new Date()); useEffect(() => { const timer = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(timer); }, []); const formatLocalTime = (tz?: string) => { if (!tz) return now.toLocaleTimeString(); try { return new Intl.DateTimeFormat("en-US", { timeZone: tz, hour: "numeric", minute: "numeric", second: "numeric", hour12: true, }).format(now); } catch { return now.toLocaleTimeString(); } }; // Convert landmass sample points into SVG grid coordinates const points = useMemo(() => { return WORLD_LANDMASS_SAMPLES.map(([lat, lng]) => { const { x, y } = latLngToXY(lat, lng, width, height); return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 }; }); }, [width, height]); // Convert markers to SVG coordinates const plottedMarkers = useMemo(() => { return markers.map((m) => { const { x, y } = latLngToXY(m.lat, m.lng, width, height); return { ...m, x, y }; }); }, [markers, width, height]); // Generate SVG Bezier arc paths const plottedArcs = useMemo(() => { return arcs.map((arc, idx) => { const start = latLngToXY(arc.start.lat, arc.start.lng, width, height); const end = latLngToXY(arc.end.lat, arc.end.lng, width, height); // Calculate control point for arching curve const dx = end.x - start.x; const dy = end.y - start.y; const dist = Math.sqrt(dx * dx + dy * dy); const midX = (start.x + end.x) / 2; const midY = (start.y + end.y) / 2 - dist * 0.25; // arch curvature height return { id: arc.id || `arc-${idx}`, path: `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`, color: arc.color || markerColor, strokeWidth: arc.strokeWidth || 1.2, }; }); }, [arcs, width, height, markerColor]); return (
{/* Subtle grid background */} {/* Continental Dotted Matrix */} {points.map((pt, i) => ( ))} {/* Connecting Curved Arcs */} {plottedArcs.map((arc) => ( {/* Traveling light particle */} ))} {/* Location Markers */} {plottedMarkers.map((marker, idx) => { const r = marker.size || 3.5; const mColor = marker.color || markerColor; const pColor = pulseColor || mColor; const shouldPulse = pulse || marker.pulse; const isHovered = hoveredMarker?.id === marker.id || (hoveredMarker?.lat === marker.lat && hoveredMarker?.lng === marker.lng); return ( interactive && setHoveredMarker(marker)} onMouseLeave={() => interactive && setHoveredMarker(null)} onClick={() => onMarkerClick?.(marker)} > {/* Pulsing radar waves */} {shouldPulse && ( )} {/* Core solid marker point */} {/* Center highlight dot */} {/* Custom Marker Overlay Hook */} {renderMarkerOverlay?.({ marker, x: marker.x, y: marker.y, r })} ); })} {/* Floating Interactive Tooltip */} {enableTooltips && hoveredMarker && (
{hoveredMarker.label || `${hoveredMarker.lat.toFixed(1)}°, ${hoveredMarker.lng.toFixed(1)}°`}
{hoveredMarker.ping && ( {hoveredMarker.ping} )}
{hoveredMarker.timeZone && (
Local Time {formatLocalTime(hoveredMarker.timeZone)}
)}
Coordinates {hoveredMarker.lat.toFixed(2)}°, {hoveredMarker.lng.toFixed(2)}°
)}
); } export default WorldMap; ``` -------------------------------------------------- ### COMPONENT: glowing-cards Category: Components Description: Interactive card components with beautiful glowing effects that follow mouse cursor movement. Perfect for feature showcases, pricing sections, and content highlights. URL: https://lightswind.com/components/glowing-cards Import: import { GlowingCards, GlowingCard } from "@/components/lightswind/glowing-cards" Registry URL: https://lightswind.com/r/glowing-cards.json Install Command: npx lightswind@latest add glowing-cards Usage: ```tsx import { GlowingCards, GlowingCard } from "@/components/lightswind/glowing-cards"; import { Zap, Sparkles, Crown } from "lucide-react"; // Basic usage

Performance

Lightning-fast components...

Design

Beautiful, accessible components...

// Advanced configuration

Premium Features

Enterprise-grade components...

``` Source Code: ```tsx "use client"; import React, { useEffect, useRef, useState } from 'react'; import { cn } from '@/components/lib/utils'; export interface GlowingCardProps { children: React.ReactNode; className?: string; glowColor?: string; hoverEffect?: boolean; } export interface GlowingCardsProps { children: React.ReactNode; className?: string; /** Enable the glowing overlay effect */ enableGlow?: boolean; /** Size of the glow effect radius */ glowRadius?: number; /** Opacity of the glow effect */ glowOpacity?: number; /** Animation duration for glow transitions */ animationDuration?: number; /** Enable hover effects on individual cards */ enableHover?: boolean; /** Gap between cards */ gap?: string; /** Maximum width of cards container */ maxWidth?: string; /** Padding around the container */ padding?: string; /** Background color for the container */ backgroundColor?: string; /** Border radius for cards */ borderRadius?: string; /** Enable responsive layout */ responsive?: boolean; /** Custom CSS variables for theming */ customTheme?: { cardBg?: string; cardBorder?: string; textColor?: string; hoverBg?: string; }; } export const GlowingCard: React.FC = ({ children, className, glowColor = "#3b82f6", hoverEffect = true, ...props }) => { return (
{children}
); }; export const GlowingCards: React.FC = ({ children, className, enableGlow = true, glowRadius = 25, glowOpacity = 1, animationDuration = 400, enableHover = true, gap = "2.5rem", maxWidth = "75rem", padding = "3rem 1.5rem", backgroundColor, borderRadius = "1rem", responsive = true, customTheme, }) => { const containerRef = useRef(null); const overlayRef = useRef(null); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); const [showOverlay, setShowOverlay] = useState(false); useEffect(() => { const container = containerRef.current; const overlay = overlayRef.current; if (!container || !overlay || !enableGlow) return; const handleMouseMove = (e: MouseEvent) => { const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setMousePosition({ x, y }); setShowOverlay(true); // Using string concatenation for style properties overlay.style.setProperty('--x', x + 'px'); overlay.style.setProperty('--y', y + 'px'); overlay.style.setProperty('--opacity', glowOpacity.toString()); }; const handleMouseLeave = () => { setShowOverlay(false); overlay.style.setProperty('--opacity', '0'); }; container.addEventListener('mousemove', handleMouseMove); container.addEventListener('mouseleave', handleMouseLeave); return () => { container.removeEventListener('mousemove', handleMouseMove); container.removeEventListener('mouseleave', handleMouseLeave); }; }, [enableGlow, glowOpacity]); const containerStyle = { '--gap': gap, '--max-width': maxWidth, '--padding': padding, '--border-radius': borderRadius, '--animation-duration': animationDuration + 'ms', // Concatenation '--glow-radius': glowRadius + 'rem', // Concatenation '--glow-opacity': glowOpacity, backgroundColor: backgroundColor || undefined, ...customTheme, } as React.CSSProperties; return (
{children}
{enableGlow && (
{React.Children.map(children, (child, index) => { if (React.isValidElement(child) && child.type === GlowingCard) { const element = child as React.ReactElement; const cardGlowColor = element.props.glowColor || "#3b82f6"; return React.cloneElement(element, { className: cn( element.props.className, "bg-opacity-15 dark:bg-opacity-15", "border-opacity-100 dark:border-opacity-100" ), style: { ...element.props.style, // String concatenation for background, border, and boxShadow backgroundColor: cardGlowColor + "15", borderColor: cardGlowColor, boxShadow: "0 0 0 1px inset " + cardGlowColor, }, }); } return child; })}
)}
); }; export { GlowingCards as default }; ``` -------------------------------------------------- ### COMPONENT: hamburger-menu-overlay Category: Components Description: A professional, fully customizable hamburger menu with animated full-screen overlay. Features smooth GSAP-style animations, responsive design, and comprehensive accessibility support. URL: https://lightswind.com/components/hamburger-menu-overlay Import: import { HamburgerMenuOverlay } from "@/components/lightswind/hamburger-menu-overlay" Registry URL: https://lightswind.com/r/hamburger-menu-overlay.json Install Command: npx lightswind@latest add hamburger-menu-overlay Usage: ```tsx import { HamburgerMenuOverlay } from "@/components/lightswind/hamburger-menu-overlay"; import { Home, Search, User, Settings } from "lucide-react"; // Basic navigation menu const menuItems = [ { label: "Home", icon: , href: "/" }, { label: "Search", icon: , href: "/search" }, { label: "Profile", icon: , onClick: () => console.log("Profile") }, { label: "Settings", icon: , href: "/settings" } ]; // Advanced with gradient and blur // Custom positioning and styling console.log("Menu opened")} onClose={() => console.log("Menu closed")} /> ``` Source Code: ```tsx "use client"; import React, { useState, useEffect, useRef } from "react"; import { cn } from "@/components/lib/utils"; import { Menu, X } from "lucide-react"; export interface MenuItem { label: string; href?: string; onClick?: () => void; icon?: React.ReactNode; } export interface HamburgerMenuOverlayProps { /** Array of menu items */ items: MenuItem[]; /** Button position from top */ buttonTop?: string; /** Button position from left */ buttonLeft?: string; /** Button size */ buttonSize?: "sm" | "md" | "lg"; /** Button background color */ buttonColor?: string; /** Overlay background color/gradient */ overlayBackground?: string; /** Menu text color */ textColor?: string; /** Menu font size */ fontSize?: "sm" | "md" | "lg" | "xl" | "2xl"; /** Font family */ fontFamily?: string; /** Font weight */ fontWeight?: "normal" | "medium" | "semibold" | "bold"; /** Animation duration in seconds */ animationDuration?: number; /** Stagger delay between menu items */ staggerDelay?: number; /** Menu items alignment */ menuAlignment?: "left" | "center" | "right"; /** Custom class for container */ className?: string; /** Custom class for button */ buttonClassName?: string; /** Custom class for menu items */ menuItemClassName?: string; /** Disable overlay close on item click */ keepOpenOnItemClick?: boolean; /** Custom button content */ customButton?: React.ReactNode; /** ARIA label for accessibility */ ariaLabel?: string; /** Callback when menu opens */ onOpen?: () => void; /** Callback when menu closes */ onClose?: () => void; /** Menu items layout direction */ menuDirection?: "vertical" | "horizontal"; /** Enable blur backdrop */ enableBlur?: boolean; /** Z-index for overlay */ zIndex?: number; } export const HamburgerMenuOverlay: React.FC = ({ items = [], buttonTop = "60px", buttonLeft = "60px", buttonSize = "md", buttonColor = "#6c8cff", overlayBackground = "#6c8cff", textColor = "#ffffff", fontSize = "md", fontFamily = '"Krona One", monospace', fontWeight = "bold", animationDuration = 1.5, staggerDelay = 0.1, menuAlignment = "left", className, buttonClassName, menuItemClassName, keepOpenOnItemClick = false, customButton, ariaLabel = "Navigation menu", onOpen, onClose, menuDirection = "vertical", enableBlur = false, zIndex = 1000, }) => { const [isOpen, setIsOpen] = useState(false); const navRef = useRef(null); const containerRef = useRef(null); const buttonSizes = { sm: "w-10 h-10", md: "w-12 h-12", lg: "w-16 h-16", }; const fontSizes = { sm: "text-2xl md:text-3xl", md: "text-3xl md:text-4xl", lg: "text-4xl md:text-5xl", xl: "text-5xl md:text-6xl", "2xl": "text-6xl md:text-7xl", }; const toggleMenu = () => { const newState = !isOpen; setIsOpen(newState); if (newState) { onOpen?.(); } else { onClose?.(); } }; const handleItemClick = (item: MenuItem) => { if (item.onClick) { item.onClick(); } if (item.href && !item.onClick) { window.location.href = item.href; } if (!keepOpenOnItemClick) { setIsOpen(false); onClose?.(); } }; // Close menu on escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && isOpen) { setIsOpen(false); onClose?.(); } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [isOpen, onClose]); return (
{/* Navigation Overlay */}
    {items.map((item, index) => (
  • handleItemClick(item)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleItemClick(item); } }} tabIndex={isOpen ? 0 : -1} role="button" aria-label={`Navigate to ${item.label}`} > {item.icon && {item.icon}} {item.label}
  • ))}
{/* Hamburger Button */}
); }; export default HamburgerMenuOverlay; ``` -------------------------------------------------- ### COMPONENT: image-reveal Category: Components Description: An animated image hover reveal component built with Framer Motion. Displays a floating image near the cursor when hovering over each label. Perfect for showcasing visual categories, portfolios, or galleries with interactive feedback. URL: https://lightswind.com/components/image-reveal Import: import ImageReveal from '@/components/lightswind/image-reveal'; Registry URL: https://lightswind.com/r/image-reveal.json Install Command: npx lightswind@latest add image-reveal Usage: ```tsx import ImageReveal from '@/components/lightswind/image-reveal'; // Basic usage ``` Source Code: ```tsx "use client"; import React, { useState, useEffect } from "react"; import { motion, useMotionValue, useSpring } from "framer-motion"; import { MoveUpRight as ArrowIcon } from "lucide-react"; interface VisualItem { key: number; url: string; label: string; } const visualData: VisualItem[] = [ { key: 1, url: "https://images.pexels.com/photos/9002742/pexels-photo-9002742.jpeg", label: "Pinky Island", }, { key: 2, url: "https://images.pexels.com/photos/31622979/pexels-photo-31622979.jpeg", label: "Greedy Model", }, { key: 3, url: "https://images.pexels.com/photos/12187128/pexels-photo-12187128.jpeg", label: "Sigma Connect", }, { key: 4, url: "https://images.pexels.com/photos/28168248/pexels-photo-28168248.jpeg", label: "Futuristic Gamma", }, ]; const ImageReveal: React.FC = () => { const [focusedItem, setFocusedItem] = useState(null); const [isLargeScreen, setIsLargeScreen] = useState(true); const cursorX = useMotionValue(0); const cursorY = useMotionValue(0); const smoothX = useSpring(cursorX, { stiffness: 300, damping: 40 }); const smoothY = useSpring(cursorY, { stiffness: 300, damping: 40 }); useEffect(() => { const updateScreen = () => { setIsLargeScreen(window.innerWidth >= 768); }; updateScreen(); window.addEventListener("resize", updateScreen); return () => window.removeEventListener("resize", updateScreen); }, []); const onMouseTrack = (e: React.MouseEvent) => { cursorX.set(e.clientX); cursorY.set(e.clientY); }; const onHoverActivate = (item: VisualItem) => { setFocusedItem(item); }; const onHoverDeactivate = () => { setFocusedItem(null); }; return (
{visualData.map((item) => (
onHoverActivate(item)} > {!isLargeScreen && ( {item.label} )}

{item.label}

))} {isLargeScreen && focusedItem && ( )}
); }; export default ImageReveal; ``` -------------------------------------------------- ### COMPONENT: image-trail-effect Category: Components Description: An animated mouse trail image effect built with React. Displays a trail of floating images that follow the user's cursor. Ideal for creating visually engaging and interactive sections on landing pages or portfolios. URL: https://lightswind.com/components/image-trail-effect Import: import ImageTrailEffect from '@/components/lightswind/image-trail-effect'; Registry URL: https://lightswind.com/r/image-trail-effect.json Install Command: npx lightswind@latest add image-trail-effect Usage: ```tsx import ImageTrailEffect from '@/components/lightswind/image-trail-effect'; Hover Me!} /> ``` Source Code: ```tsx // @ts-nocheck "use client"; import { cn } from "@/components/lib/utils"; import { createRef, ReactNode, useRef } from "react"; interface MouseTrailProps { imageSources: string[]; content?: ReactNode; containerClassName?: string; imageClassName?: string; triggerDistance?: number; maxTrailImages?: number; useFadeEffect?: boolean; } export default function ImageTrailEffect({ imageSources, content, containerClassName, maxTrailImages = 5, imageClassName = "w-40 h-48", triggerDistance = 20, useFadeEffect = false, }: MouseTrailProps) { const wrapperRef = useRef(null); const imageRefs = useRef( imageSources.map(() => createRef()) ); const zIndexCounterRef = useRef(1); let imageIndex = 0; let lastPosition = { x: 0, y: 0 }; const activateImage = (img: HTMLImageElement, x: number, y: number) => { const containerBounds = wrapperRef.current?.getBoundingClientRect(); const relativeX = x - containerBounds.left; const relativeY = y - containerBounds.top; img.style.left = `${relativeX}px`; img.style.top = `${relativeY}px`; if (zIndexCounterRef.current > 40) { zIndexCounterRef.current = 1; } img.style.zIndex = String(zIndexCounterRef.current++); img.dataset.status = "active"; if (useFadeEffect) { setTimeout(() => { img.dataset.status = "inactive"; }, 1500); } lastPosition = { x, y }; }; const calculateDistance = (x: number, y: number) => { return Math.hypot(x - lastPosition.x, y - lastPosition.y); }; const deactivateImage = (img: HTMLImageElement) => { img.dataset.status = "inactive"; }; const handleMouseMove = (e: MouseEvent | Touch) => { if ( calculateDistance(e.clientX, e.clientY) > window.innerWidth / triggerDistance ) { const leadImage = imageRefs.current[imageIndex % imageRefs.current.length]?.current; const tailImage = imageRefs.current[ (imageIndex - maxTrailImages) % imageRefs.current.length ]?.current; if (leadImage) activateImage(leadImage, e.clientX, e.clientY); if (tailImage) deactivateImage(tailImage); imageIndex++; } }; const handleMouseLeave = () => { // Deactivate all images when cursor leaves the container imageRefs.current.forEach((ref) => { if (ref.current) { ref.current.dataset.status = "inactive"; } }); // Reset position tracker so next entry restarts cleanly lastPosition = { x: 0, y: 0 }; }; return (
handleMouseMove(e.touches[0])} ref={wrapperRef} className={cn( `grid place-content-center h-[600px] w-full bg-background text-foreground relative overflow-hidden rounded-lg`, containerClassName )} > {imageSources.map((src, i) => ( {`trail-${i}`} ))} {content}
); } ``` -------------------------------------------------- ### COMPONENT: interactive-card Category: Components Description: A responsive and animated 3D hover card built with Framer Motion and Tailwind CSS. It responds to cursor position with rotation and a glowing radial gradient. Perfect for enhancing UI with interactive visual depth and modern appeal. URL: https://lightswind.com/components/interactive-card Import: import { InteractiveCard } from '@/components/lightswind/interactive-card'; Registry URL: https://lightswind.com/r/interactive-card.json Install Command: npx lightswind@latest add interactive-card Usage: ```tsx import { InteractiveCard } from '@/components/lightswind/interactive-card'; Hover Me! ; ``` Source Code: ```tsx import { useRef, useState } from "react"; import { motion, useMotionValue, useTransform, useMotionTemplate } from "framer-motion"; import { cn } from "@/components/lib/utils"; // Assuming cn is a utility for conditionally joining class names export const InteractiveCard = ({ children, className, InteractiveColor = "#07eae6ff", // backgroundColor = "#0c0d16", // whiteCardBackgroundColor = "#173eff", borderRadius = "48px", rotationFactor = 0.4, transitionDuration = 0.3, transitionEasing = "easeInOut", // Add a prop to accept Tailwind background classes // Example: "bg-gray-900 dark:bg-gray-800" tailwindBgClass = "bg-transparent backdrop-blur-md", }: { children: React.ReactNode; className?: string; InteractiveColor?: string; // If you're using tailwindBgClass, you might not need these anymore, // or you could use them as fallbacks/defaults // backgroundColor?: string; // whiteCardBackgroundColor?: string; borderRadius?: string; rotationFactor?: number; transitionDuration?: number; transitionEasing?: string; tailwindBgClass?: string; // Prop to accept Tailwind background classes }) => { const cardRef = useRef(null); const [isHovered, setIsHovered] = useState(false); const x = useMotionValue(0); const y = useMotionValue(0); const rotateXTrans = useTransform(y, [0, 1], [rotationFactor * 15, -rotationFactor * 15]); const rotateYTrans = useTransform(x, [0, 1], [-rotationFactor * 15, rotationFactor * 15]); const handlePointerMove = (e: React.PointerEvent) => { const bounds = cardRef.current?.getBoundingClientRect(); if (!bounds) return; const px = (e.clientX - bounds.left) / bounds.width; const py = (e.clientY - bounds.top) / bounds.height; x.set(px); y.set(py); }; const xPercentage = useTransform(x, (val) => `${val * 100}%`); const yPercentage = useTransform(y, (val) => `${val * 100}%`); const interactiveBackground = useMotionTemplate`radial-gradient(circle at ${xPercentage} ${yPercentage}, ${InteractiveColor} 0%, transparent 80%)`; return ( setIsHovered(true)} onPointerLeave={() => setIsHovered(false)} style={{ perspective: 1000, borderRadius, }} className="relative w-[320px] aspect-[17/21] isolate" > {/* Background Interactive Layer */} {/* Content */}
{children}
); }; ``` -------------------------------------------------- ### COMPONENT: interactive-gradient-card Category: Components Description: A customizable interactive gradient background that responds to mouse movements and provides visual feedback. URL: https://lightswind.com/components/interactive-gradient-card Import: import { InteractiveGradient } from "@/components/lightswind/interactive-gradient" Registry URL: https://lightswind.com/r/interactive-gradient-card.json Install Command: npx lightswind@latest add interactive-gradient-card Usage: ```tsx import { InteractiveGradient } from "@/components/lightswind/interactive-gradient" // Basic usage

Interactive Card

Hover or move your mouse around to see the gradient effect

// With custom colors and options
Content here
``` Source Code: ```tsx import React, { useEffect, useRef, useState } from "react"; import { cn } from "@/components/lib/utils"; export interface GradientCardProps { color: string; glowColor: string; width?: string; height?: string; borderRadius?: string; className?: string; children?: React.ReactNode; followMouse?: boolean; hoverOnly?: boolean; intensity?: number; backgroundColor?: string; // Keep this prop for other potential uses or if you want to pass a raw CSS color string } export const InteractiveGradient = ({ color , glowColor = "#107667ed", width = "", height = "", borderRadius = "1rem", className = "", children, followMouse = true, hoverOnly = false, intensity = 100, backgroundColor, // You can still use this for other parts if needed }: GradientCardProps) => { const cardRef = useRef(null); const [position, setPosition] = useState({ x: 0, y: 0 }); const [isHovering, setIsHovering] = useState(false); const [resolvedGlowFallbackColor, setResolvedGlowFallbackColor] = useState("#ffffff"); // Renamed for clarity // Detect dark mode for fallback (if backgroundColor prop isn't a direct CSS color) useEffect(() => { // This logic is primarily for the *glow fallback* when the background is handled by Tailwind // or when no direct backgroundColor is provided for the radial gradient's base. if (!backgroundColor || !backgroundColor.startsWith("#") && !backgroundColor.startsWith("rgb")) { const html = document.documentElement; const updateColor = () => { const isDark = html.classList.contains("dark"); // This sets the color for the "base" of the radial gradient, which needs a CSS color. setResolvedGlowFallbackColor(isDark ? "#000" : "#ffffff"); // Black for dark, white for light as default }; updateColor(); const observer = new MutationObserver(updateColor); observer.observe(html, { attributes: true, attributeFilter: ["class"] }); return () => observer.disconnect(); } else { // If backgroundColor is a direct CSS color, use it for the radial gradient's base. setResolvedGlowFallbackColor(backgroundColor); } }, [backgroundColor]); const normalizedIntensity = Math.max(0, Math.min(100, intensity)) / 100; useEffect(() => { if (!followMouse) return; const handleMouseMove = (e: MouseEvent) => { if (!cardRef.current || (hoverOnly && !isHovering)) return; const rect = cardRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setPosition({ x, y }); }; window.addEventListener("mousemove", handleMouseMove); return () => window.removeEventListener("mousemove", handleMouseMove); }, [followMouse, hoverOnly, isHovering]); const getBackgroundStyle = (): React.CSSProperties => { // Use resolvedGlowFallbackColor for the radial gradient's base, // which should always be a valid CSS color. if (!followMouse || (hoverOnly && !isHovering)) { return { background: `radial-gradient(circle at center, ${glowColor} 0%, ${resolvedGlowFallbackColor} ${45 * normalizedIntensity}%, ${resolvedGlowFallbackColor} 100%)`, }; } return { background: `radial-gradient(circle at ${position.x}px ${position.y}px, ${glowColor} 0%, ${resolvedGlowFallbackColor} ${45 * normalizedIntensity}%, ${resolvedGlowFallbackColor} 100%)`, }; }; const getBorderStyle = (): React.CSSProperties => { // resolvedGlowFallbackColor is also used here return { "--gradient-border": `linear-gradient(45deg, ${resolvedGlowFallbackColor}, ${resolvedGlowFallbackColor}, ${color})`, } as React.CSSProperties; }; return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} > {/* Gradient border */} {children}
); }; export default InteractiveGradient; ``` -------------------------------------------------- ### COMPONENT: shader-card Category: Components Description: An interactive WebGL fluid plasma shader card with cursor turbulence displacement, dynamic lighting glow, and 3D perspective tilt. URL: https://lightswind.com/components/shader-card Import: import ShaderCard from "@/components/lightswind/shader-card"; Registry URL: https://lightswind.com/r/shader-card.json Install Command: npx lightswind@latest add shader-card Usage: ```tsx import ShaderCard from "@/components/lightswind/shader-card"; import { Sparkles, Check, ArrowRight } from "lucide-react"; export default function Example() { return (
Pro Tier

Enterprise AI Suite

GPU shader acceleration for modern applications.

$49 / mo
); } ``` Source Code: ```tsx "use client"; import React, { useRef, useEffect, useCallback, useState, PropsWithChildren, CSSProperties, forwardRef, useImperativeHandle, } from "react"; import { useTheme } from "next-themes"; import { cn } from "@/components/lib/utils"; export interface ShaderCardProps extends PropsWithChildren { /** Primary electric plasma shader color (Hex or RGB string, default: "#00D2FF" Sky Blue) */ color?: string; /** Background card interior base color (default: "#0d0e12") */ bgColor?: string; /** Fluid wave speed multiplier (default: 0.5) */ speed?: number; /** Vertical origin position offset (default: 0.15) */ positionY?: number; /** Coordinate zoom scale factor (default: 4.0) */ scale?: number; /** Power exponent controlling plasma branch contrast & sharpness (default: 1.8) */ branchIntensity?: number; /** Horizontal wave distortion amplitude (default: 0.25) */ waveAmount?: number; /** Noise granularity and density multiplier (default: 1.5) */ noiseScale?: number; /** Exponential vertical falloff power (default: 1.6) */ falloffPower?: number; /** WebGL shader layer opacity (0.0 to 1.0, default: 0.85) */ opacity?: number; /** Optional blur filter applied directly to the WebGL shader canvas (e.g. "6px" or 6) */ shaderBlur?: number | string; /** Add a mild frosted glass blur overlay on top of the shader (default: false) */ frostedOverlay?: boolean; /** Enable cursor-reactive turbulence and wave displacement (default: true) */ interactive?: boolean; /** Enable subtle 3D card tilt perspective when hovering (default: true) */ enableTilt?: boolean; /** Maximum 3D tilt angle in degrees (default: 10) */ maxTilt?: number; /** Enable atmospheric colored ambient glow behind card (default: true) */ glow?: boolean; /** Glow blur radius in pixels (default: 32) */ glowBlur?: number; /** Glow opacity (default: 0.35) */ glowOpacity?: number; /** Border radius for the card (default: "20px") */ radius?: string | number; /** Optional outer card container class */ className?: string; /** Optional canvas element class */ canvasClassName?: string; /** Optional inline styles */ style?: CSSProperties; /** Maximum device pixel ratio (default: 2) */ dpr?: number; } export interface ShaderCardHandle { getCanvas: () => HTMLCanvasElement | null; getGL: () => WebGLRenderingContext | null; } /** Utility to parse Hex or RGB strings to normalized [r, g, b] float vectors */ function parseColorToRgb(color: string, fallback: [number, number, number] = [0, 0.82, 1]): [number, number, number] { if (!color) return fallback; const clean = color.trim(); if (clean.startsWith("#")) { let hex = clean.replace("#", ""); if (hex.length === 3) { hex = hex.split("").map((c) => c + c).join(""); } const num = parseInt(hex, 16); if (isNaN(num)) return fallback; return [ ((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255, ]; } const match = clean.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (match) { return [ parseInt(match[1], 10) / 255, parseInt(match[2], 10) / 255, parseInt(match[3], 10) / 255, ]; } return fallback; } const VERTEX_SHADER = ` attribute vec2 a_position; varying vec2 v_uv; void main() { v_uv = (a_position + 1.0) * 0.5; gl_Position = vec4(a_position, 0.0, 1.0); } `; const FRAGMENT_SHADER = ` precision highp float; varying vec2 v_uv; uniform float u_time; uniform vec2 u_resolution; uniform vec3 u_color; uniform float u_speed; uniform float u_positionY; uniform float u_scale; uniform float u_branchIntensity; uniform float u_waveAmount; uniform float u_noiseScale; uniform float u_falloffPower; uniform float u_opacity; uniform vec2 u_mouse; uniform float u_isHovered; // Simple 2D Pseudo Random / Noise float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f); float a = hash(i); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0)); return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } float fbm(vec2 p) { float v = 0.0; float a = 0.5; for (int i = 0; i < 4; i++) { v += a * noise(p); p *= 2.0; a *= 0.5; } return v; } void main() { vec2 uv = v_uv; float t = u_time * u_speed; // Aspect ratio correction float aspect = u_resolution.x / u_resolution.y; // Position offset adjustment vec2 p = uv * u_scale; p.y -= u_positionY * u_scale; // Cursor interactive fluid wave displacement if (u_isHovered > 0.0) { vec2 m = u_mouse * u_scale; m.y -= u_positionY * u_scale; float dMouse = distance(p, m); float mouseInfluence = smoothstep(1.6, 0.0, dMouse); p += (p - m) * mouseInfluence * 0.5 * u_isHovered; } // Wave distortion float wave = sin(p.x * 2.0 + t) * cos(p.y * 1.5 + t * 0.7) * u_waveAmount; // Fluid noise branch calculation float n = fbm(p * u_noiseScale + vec2(t * 0.2, -t * 0.3) + wave); n = pow(n, u_branchIntensity); // Vertical falloff mask (glow concentrated around bottom-to-middle) float verticalMask = smoothstep(0.0, 1.0, uv.y * 1.6); verticalMask = pow(verticalMask, u_falloffPower); // Core glow & color mixing float intensity = n * verticalMask * 2.4; vec3 color = u_color * intensity; // Bottom accent glow float bottomGlow = smoothstep(0.0, 0.8, uv.y) * 0.35; color += u_color * bottomGlow; // Localized cursor interactive glow aura if (u_isHovered > 0.0) { float dMouseUV = distance(uv, u_mouse); float cursorGlow = smoothstep(0.45, 0.0, dMouseUV) * 0.35 * u_isHovered; color += u_color * cursorGlow; } float alpha = clamp(intensity * u_opacity, 0.0, 1.0); gl_FragColor = vec4(color, alpha); } `; function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null { const shader = gl.createShader(type); if (!shader) return null; gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error("Shader compile error:", gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } export const ShaderCard = forwardRef(({ children, color = "#00D2FF", bgColor, speed = 0.5, positionY = 0.15, scale = 4.0, branchIntensity = 1.8, waveAmount = 0.25, noiseScale = 1.5, falloffPower = 1.6, opacity = 0.85, shaderBlur, frostedOverlay = false, interactive = true, enableTilt = false, maxTilt = 10, glow = true, glowBlur = 32, glowOpacity = 0.35, radius = "20px", className, canvasClassName, style, dpr = 2, }, ref) => { const containerRef = useRef(null); const canvasRef = useRef(null); const glRef = useRef(null); const animFrameRef = useRef(null); const isVisibleRef = useRef(true); const mousePosRef = useRef<{ x: number; y: number }>({ x: 0.5, y: 0.5 }); const isHoveredRef = useRef(0); const startTimeRef = useRef(performance.now()); const [tiltStyle, setTiltStyle] = useState<{ transform: string }>({ transform: "" }); const { resolvedTheme, theme } = useTheme(); const isLightMode = resolvedTheme === "light" || theme === "light"; const effectiveBgColor = bgColor ?? (isLightMode ? "#f8fafc" : "#0d0e12"); const parsedRadius = typeof radius === "number" ? `${radius}px` : radius; const parsedShaderBlur = typeof shaderBlur === "number" ? `${shaderBlur}px` : shaderBlur; useImperativeHandle(ref, () => ({ getCanvas: () => canvasRef.current, getGL: () => glRef.current, })); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const gl = canvas.getContext("webgl", { alpha: true, antialias: true, depth: false, preserveDrawingBuffer: false, }); if (!gl) { console.warn("WebGL not supported for ShaderCard"); return; } glRef.current = gl; gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); const vs = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER); const fs = createShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER); if (!vs || !fs) return; const program = gl.createProgram(); if (!program) return; gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error("Program link error:", gl.getProgramInfoLog(program)); return; } gl.useProgram(program); // Quad Geometry Buffers const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1, ]), gl.STATIC_DRAW ); const positionLocation = gl.getAttribLocation(program, "a_position"); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); // Uniform Locations const uniforms = { time: gl.getUniformLocation(program, "u_time"), resolution: gl.getUniformLocation(program, "u_resolution"), color: gl.getUniformLocation(program, "u_color"), speed: gl.getUniformLocation(program, "u_speed"), positionY: gl.getUniformLocation(program, "u_positionY"), scale: gl.getUniformLocation(program, "u_scale"), branchIntensity: gl.getUniformLocation(program, "u_branchIntensity"), waveAmount: gl.getUniformLocation(program, "u_waveAmount"), noiseScale: gl.getUniformLocation(program, "u_noiseScale"), falloffPower: gl.getUniformLocation(program, "u_falloffPower"), opacity: gl.getUniformLocation(program, "u_opacity"), mouse: gl.getUniformLocation(program, "u_mouse"), isHovered: gl.getUniformLocation(program, "u_isHovered"), }; const handleResize = () => { if (!canvas || !gl) return; const targetDpr = Math.min(window.devicePixelRatio || 1, dpr); const displayWidth = Math.round(canvas.clientWidth * targetDpr); const displayHeight = Math.round(canvas.clientHeight * targetDpr); if (canvas.width !== displayWidth || canvas.height !== displayHeight) { canvas.width = Math.max(1, displayWidth); canvas.height = Math.max(1, displayHeight); gl.viewport(0, 0, canvas.width, canvas.height); } }; handleResize(); const resizeObserver = new ResizeObserver(() => handleResize()); resizeObserver.observe(canvas); const intersectionObserver = new IntersectionObserver( ([entry]) => { isVisibleRef.current = entry.isIntersecting; }, { threshold: 0.05 } ); intersectionObserver.observe(canvas); let currentHover = 0; const render = () => { if (isVisibleRef.current && gl && canvas) { handleResize(); const time = (performance.now() - startTimeRef.current) * 0.001; const rgb = parseColorToRgb(color, [0, 0.82, 1]); // Smooth hover transition const targetHover = isHoveredRef.current; currentHover += (targetHover - currentHover) * 0.1; gl.uniform1f(uniforms.time, time); gl.uniform2f(uniforms.resolution, canvas.width, canvas.height); gl.uniform3f(uniforms.color, rgb[0], rgb[1], rgb[2]); gl.uniform1f(uniforms.speed, speed); gl.uniform1f(uniforms.positionY, positionY); gl.uniform1f(uniforms.scale, scale); gl.uniform1f(uniforms.branchIntensity, branchIntensity); gl.uniform1f(uniforms.waveAmount, waveAmount); gl.uniform1f(uniforms.noiseScale, noiseScale); gl.uniform1f(uniforms.falloffPower, falloffPower); gl.uniform1f(uniforms.opacity, opacity); gl.uniform2f(uniforms.mouse, mousePosRef.current.x, mousePosRef.current.y); gl.uniform1f(uniforms.isHovered, currentHover); gl.drawArrays(gl.TRIANGLES, 0, 6); } animFrameRef.current = requestAnimationFrame(render); }; animFrameRef.current = requestAnimationFrame(render); return () => { if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current); resizeObserver.disconnect(); intersectionObserver.disconnect(); if (gl) { gl.deleteProgram(program); gl.deleteShader(vs); gl.deleteShader(fs); gl.deleteBuffer(positionBuffer); } }; }, [ color, speed, positionY, scale, branchIntensity, waveAmount, noiseScale, falloffPower, opacity, dpr, ]); const handleMouseMove = useCallback((e: React.MouseEvent) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); mousePosRef.current = { x, y: 1.0 - y }; // Invert for WebGL coords if (enableTilt) { const tiltY = (x - 0.5) * (maxTilt * 2); const tiltX = (0.5 - y) * (maxTilt * 2); setTiltStyle({ transform: `perspective(1000px) rotateX(${tiltX}deg) rotateY(${tiltY}deg) scale3d(1.02, 1.02, 1.02)`, }); } }, [enableTilt, maxTilt]); const handleMouseEnter = useCallback(() => { if (interactive) isHoveredRef.current = 1.0; }, [interactive]); const handleMouseLeave = useCallback(() => { if (interactive) isHoveredRef.current = 0.0; if (enableTilt) { setTiltStyle({ transform: "perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)", }); } }, [interactive, enableTilt]); return (
{/* Ambient Atmosphere Glow Backdrop */} {glow && (
)} {/* WebGL Canvas Background */} {/* Mild Frosted Glass Light Blur Overlay */} {frostedOverlay && (
)} {/* Content Slot Layer */} {children && (
{children}
)}
); }); ShaderCard.displayName = "ShaderCard"; export default ShaderCard; ``` -------------------------------------------------- ### COMPONENT: iphone16-pro Category: Components Description: A high-fidelity, reusable iPhone 16 Pro SVG component built with React and TypeScript. It supports displaying images or videos on the screen, a dynamic island, camera dot, and customizable frame, bezel, and screen properties. Ideal for showcasing mobile app mockups, interactive demos, or marketing visuals with realistic device frames. URL: https://lightswind.com/components/iphone16-pro Import: import { Iphone16Pro } from '@/components/lightswind/iphone16-pro'; Registry URL: https://lightswind.com/r/iphone16-pro.json Install Command: npx lightswind@latest add iphone16-pro Usage: ```tsx import { Iphone16Pro } from '@/components/lightswind/iphone16-pro'; ``` Source Code: ```tsx import React, { forwardRef, CSSProperties, SVGProps } from "react"; import { Wifi, Battery, Signal } from "lucide-react"; export type Iphone16ProFinish = "theme" | "black" | "white" | "natural" | "desert" | "cosmic" | string; export interface Iphone16ProProps extends SVGProps { /** Device frame width (base SVG width) */ width?: number; /** Device frame height (base SVG height) */ height?: number; /** Titanium finish preset ("theme" | "black" | "white" | "natural" | "desert" | "cosmic") or custom hex color */ finish?: Iphone16ProFinish; /** Image URL source for screen */ src?: string; /** Video URL source for screen */ videoSrc?: string; /** Custom wallpaper gradient or color for screen */ wallpaper?: string; /** Custom React nodes/children to render inside the iPhone screen */ children?: React.ReactNode; /** Toggle Dynamic Island pill */ showIsland?: boolean; /** Custom content inside expanded Dynamic Island on hover */ islandContent?: React.ReactNode; /** Island width */ islandWidth?: number; /** Island height */ islandHeight?: number; /** Frame color (light mode override) */ frameColor?: string; /** Frame color (dark mode override) */ frameDarkColor?: string; /** Bezel color */ bezelColor?: string; /** Screen border radius */ screenRadius?: number; /** Enable drop shadow */ shadow?: boolean; /** Rounded outer frame corners */ rounded?: boolean; /** Class for inner content container */ contentClassName?: string; /** Custom styles for inner content */ contentStyle?: CSSProperties; /** Toggle camera lens dot */ showCamera?: boolean; /** Background gradient for screen */ screenGradient?: string; /** Enable smooth hover tilt animation */ hoverAnimation?: boolean; /** Rotation angle in degrees (e.g. 0, 15, -15, 90) */ rotate?: number; /** Enable continuous smooth floating & rotating 3D animation */ autoRotate?: boolean; /** Device orientation: "portrait" | "landscape" */ orientation?: "portrait" | "landscape"; /** Toggle top iOS Status Bar (Time, WiFi, Battery) */ showStatusBar?: boolean; /** Custom time string for status bar (default: "9:41") */ statusTime?: string; /** Toggle bottom iOS Home Indicator bar */ showHomeIndicator?: boolean; /** Toggle subtle glass glare reflection overlay */ glassReflection?: boolean; } const FINISH_PRESETS: Record = { theme: { frame: "fill-black stroke-neutral-800 dark:fill-neutral-100 dark:stroke-neutral-300", stroke: "stroke-neutral-800 dark:stroke-neutral-300", bezel: "fill-black", }, black: { frame: "fill-[#141416] dark:fill-[#0a0a0c]", stroke: "stroke-[#2a2a2d] dark:stroke-[#202022]", bezel: "fill-black", }, white: { frame: "fill-[#ffffff] dark:fill-[#f0f0f4]", stroke: "stroke-[#e2e2e7] dark:stroke-[#d0d0d5]", bezel: "fill-black", }, natural: { frame: "fill-[#c5c2bb] dark:fill-[#7a7771]", stroke: "stroke-[#dfdcd6] dark:stroke-[#5a5752]", bezel: "fill-[#141416]", }, desert: { frame: "fill-[#e2d5c3] dark:fill-[#8a7d6e]", stroke: "stroke-[#f2e7d7] dark:stroke-[#695d4e]", bezel: "fill-[#141416]", }, cosmic: { frame: "fill-[#f97316] dark:fill-[#ea580c]", stroke: "stroke-[#fdba74] dark:stroke-[#c2410c]", bezel: "fill-[#141416]", }, }; export const Iphone16Pro = forwardRef( ( { width = 433, height = 882, finish = "theme", rotate = 0, autoRotate = false, orientation = "portrait", src, videoSrc, wallpaper = "bg-gradient-to-tr from-slate-950 via-indigo-950 to-slate-900", children, showIsland = true, islandContent, islandWidth = 124, islandHeight = 35, frameColor, frameDarkColor, bezelColor = "black", screenRadius = 50, shadow = true, rounded = true, contentClassName = "", contentStyle, showCamera = true, screenGradient, hoverAnimation = true, showStatusBar = true, statusTime = "9:41", showHomeIndicator = true, glassReflection = true, className = "", style, ...props }: Iphone16ProProps, ref ) => { const finishStyle = FINISH_PRESETS[finish] || { frame: frameColor ? `fill-[${frameColor}]` : FINISH_PRESETS.theme.frame, stroke: "stroke-gray-300 dark:stroke-white/20", bezel: "fill-black", }; const finalRotate = orientation === "landscape" ? rotate + 90 : rotate; const screenX = 18; const screenY = 18; const screenW = width - 36; // 397 const screenH = height - 36; // 846 return ( {autoRotate && ( )} {/* ── Side Buttons (iPhone 16 Pro Layout) ── */} {/* Action Button (Left) */} {/* Volume Up (Left) */} {/* Volume Down (Left) */} {/* Power Button (Right) */} {/* Camera Control Button (Right - New in iPhone 16) */} {/* ── Outer Titanium Frame ── */} {/* Inner Antenna/Chassis Bezel Accent */} {/* Screen Clip Path */} {/* ── Screen Area (foreignObject for Full React / Image / Video Rendering) ── */}
{/* Background Image / Video if provided */} {src && ( iPhone Screen )} {videoSrc && (