{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "liquid-ocean", "type": "registry:ui", "dependencies": [], "files": [ { "path": "components/ui/liquid-ocean.tsx", "content": "\"use client\";\r\n\r\nimport React, { useRef, useMemo, useEffect } from \"react\";\r\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\";\r\nimport { RectAreaLightUniformsLib } from \"three/examples/jsm/lights/RectAreaLightUniformsLib.js\";\r\nimport * as THREE from \"three\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n// Initialize RectAreaLight uniforms\r\nif (typeof window !== \"undefined\") {\r\n RectAreaLightUniformsLib.init();\r\n}\r\n\r\n// =============================================================================\r\n// DEFAULT THEME (Original \"Little Boxes\" Style - DO NOT CHANGE)\r\n// =============================================================================\r\nconst DEFAULT_THEME = {\r\n background: 0x000000,\r\n gridColor: 0x333333,\r\n accentColor: 0xF00589, // The original pinkish color\r\n};\r\n\r\n// =============================================================================\r\n// OCEAN MESH COMPONENT\r\n// =============================================================================\r\ninterface OceanMeshProps {\r\n geoSize: number;\r\n geoFragments: number;\r\n waveAmplitude: number;\r\n waveSpeed: number;\r\n accentColor: number;\r\n showWireframe: boolean;\r\n opacity: number;\r\n}\r\n\r\nfunction OceanMesh({\r\n geoSize,\r\n geoFragments,\r\n waveAmplitude,\r\n waveSpeed,\r\n accentColor,\r\n showWireframe,\r\n opacity,\r\n}: OceanMeshProps) {\r\n const meshRef = useRef(null);\r\n const wireRef = useRef(null);\r\n\r\n const { geometry, waves } = useMemo(() => {\r\n const geo = new THREE.PlaneGeometry(geoSize, geoSize, geoFragments, geoFragments);\r\n const positionAttribute = geo.getAttribute(\"position\");\r\n const waveData: Array<{\r\n x: number;\r\n y: number;\r\n z: number;\r\n ang: number;\r\n amp: number;\r\n speed: number;\r\n }> = [];\r\n\r\n for (let i = 0; i < positionAttribute.count; i++) {\r\n waveData.push({\r\n x: positionAttribute.getX(i),\r\n y: positionAttribute.getY(i),\r\n z: positionAttribute.getZ(i),\r\n ang: Math.PI * 2,\r\n amp: Math.random() * waveAmplitude,\r\n speed: 0.03 + Math.random() * waveSpeed,\r\n });\r\n }\r\n\r\n return { geometry: geo, waves: waveData };\r\n }, [geoSize, geoFragments, waveAmplitude, waveSpeed]);\r\n\r\n useFrame(() => {\r\n if (!meshRef.current) return;\r\n\r\n const positionAttribute = meshRef.current.geometry.getAttribute(\"position\");\r\n\r\n for (let i = 0; i < positionAttribute.count; i++) {\r\n const wave = waves[i];\r\n positionAttribute.setX(i, wave.x + Math.cos(wave.ang) * wave.amp);\r\n positionAttribute.setY(i, wave.y + Math.sin(wave.ang / 2) * wave.amp);\r\n positionAttribute.setZ(i, wave.z + Math.cos(wave.ang / 3) * wave.amp);\r\n wave.ang += wave.speed;\r\n }\r\n\r\n positionAttribute.needsUpdate = true;\r\n });\r\n\r\n const wireframeMaterial = useMemo(\r\n () =>\r\n new THREE.MeshPhysicalMaterial({\r\n color: accentColor,\r\n wireframe: true,\r\n transparent: false,\r\n opacity: 1,\r\n }),\r\n [accentColor]\r\n );\r\n\r\n const surfaceMaterial = useMemo(\r\n () =>\r\n new THREE.MeshPhysicalMaterial({\r\n color: accentColor,\r\n transparent: true,\r\n opacity: opacity,\r\n wireframe: false,\r\n }),\r\n [accentColor, opacity]\r\n );\r\n\r\n return (\r\n \r\n \r\n {showWireframe && (\r\n \r\n )}\r\n \r\n );\r\n}\r\n\r\n// =============================================================================\r\n// BOAT / FLOATING BOXES COMPONENT\r\n// =============================================================================\r\ninterface BoatData {\r\n position: [number, number, number];\r\n scale: [number, number, number];\r\n rotationY: number;\r\n vel: number;\r\n amp: number;\r\n pos: number;\r\n}\r\n\r\nfunction Boat({ data, color }: { data: BoatData; color: number }) {\r\n const meshRef = useRef(null);\r\n\r\n useFrame(({ clock }) => {\r\n if (!meshRef.current) return;\r\n const time = clock.getElapsedTime() * 3;\r\n\r\n meshRef.current.rotation.z = (Math.sin(time / data.vel) * data.amp * Math.PI) / 180;\r\n meshRef.current.rotation.x = (Math.cos(time) * data.vel * Math.PI) / 180;\r\n meshRef.current.position.y = Math.sin(time / data.vel) * data.pos;\r\n });\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n );\r\n}\r\n\r\nfunction BoatGroup({ count, spreadRange, color }: { count: number; spreadRange: number; color: number }) {\r\n const boats = useMemo(() => {\r\n const items: BoatData[] = [];\r\n for (let i = 0; i < count; i++) {\r\n const x = -Math.random() * spreadRange + Math.random() * spreadRange;\r\n const z = -Math.random() * spreadRange + Math.random() * spreadRange;\r\n const sX = Math.random();\r\n const sY = 0.5 + Math.random() * 2;\r\n\r\n items.push({\r\n position: [x, 0, z],\r\n scale: [sX, sY, sX],\r\n rotationY: (Math.random() * 360 * Math.PI) / 180,\r\n vel: 1 + Math.random() * 4,\r\n amp: 1 + Math.random() * 6,\r\n pos: Math.random() * 0.2,\r\n });\r\n }\r\n return items;\r\n }, [count, spreadRange]);\r\n\r\n return (\r\n \r\n {boats.map((boat, i) => (\r\n \r\n ))}\r\n \r\n );\r\n}\r\n\r\n// =============================================================================\r\n// SCENE CONTENT\r\n// =============================================================================\r\ninterface SceneContentProps {\r\n backgroundColor: number;\r\n gridColor: number;\r\n accentColor: number;\r\n rotationSpeed: number;\r\n showGrid: boolean;\r\n showBoats: boolean;\r\n boatCount: number;\r\n boatSpread: number;\r\n oceanSize: number;\r\n oceanFragments: number;\r\n waveAmplitude: number;\r\n waveSpeed: number;\r\n showWireframe: boolean;\r\n oceanOpacity: number;\r\n}\r\n\r\nfunction SceneContent({\r\n backgroundColor,\r\n gridColor,\r\n accentColor,\r\n rotationSpeed,\r\n showGrid,\r\n showBoats,\r\n boatCount,\r\n boatSpread,\r\n oceanSize,\r\n oceanFragments,\r\n waveAmplitude,\r\n waveSpeed,\r\n showWireframe,\r\n oceanOpacity,\r\n}: SceneContentProps) {\r\n const { scene, camera } = useThree();\r\n const rectLightRef = useRef(null);\r\n const groupRef = useRef(null);\r\n\r\n useEffect(() => {\r\n scene.fog = new THREE.Fog(backgroundColor, 5, 20);\r\n scene.background = new THREE.Color(backgroundColor);\r\n }, [scene, backgroundColor]);\r\n\r\n useFrame(() => {\r\n camera.lookAt(0, 0, 0);\r\n if (rectLightRef.current) {\r\n rectLightRef.current.lookAt(0, 0, 0);\r\n }\r\n if (groupRef.current) {\r\n groupRef.current.rotation.y += rotationSpeed;\r\n }\r\n });\r\n\r\n return (\r\n <>\r\n {/* Lighting - Original setup */}\r\n \r\n \r\n \r\n \r\n\r\n {/* Rotating Group */}\r\n \r\n {showGrid && }\r\n {showBoats && }\r\n \r\n \r\n \r\n );\r\n}\r\n\r\n// =============================================================================\r\n// MAIN COMPONENT - Props for reusability, defaults match original exactly\r\n// =============================================================================\r\nexport interface LiquidOceanProps {\r\n /** Additional CSS classes */\r\n className?: string;\r\n /** Background color as hex number (default: 0x000000 - black) */\r\n backgroundColor?: number;\r\n /** Grid line color as hex number (default: 0x333333) */\r\n gridColor?: number;\r\n /** Accent color for ocean, boats, lights as hex number (default: 0xF00589 - pink) */\r\n accentColor?: number;\r\n /** Camera field of view (default: 20) */\r\n fov?: number;\r\n /** Scene rotation speed per frame (default: 0.001) */\r\n rotationSpeed?: number;\r\n /** Show grid helper (default: true) */\r\n showGrid?: boolean;\r\n /** Show floating boxes/boats (default: true) */\r\n showBoats?: boolean;\r\n /** Number of floating boxes (default: 5) */\r\n boatCount?: number;\r\n /** Spread range for floating boxes (default: 5) */\r\n boatSpread?: number;\r\n /** Ocean plane size (default: 25) */\r\n oceanSize?: number;\r\n /** Ocean geometry fragments/subdivisions (default: 25) */\r\n oceanFragments?: number;\r\n /** Maximum wave amplitude (default: 0.2) */\r\n waveAmplitude?: number;\r\n /** Wave animation speed multiplier (default: 0.05) */\r\n waveSpeed?: number;\r\n /** Show wireframe overlay on ocean (default: true) */\r\n showWireframe?: boolean;\r\n /** Ocean surface opacity (default: 0.85) */\r\n oceanOpacity?: number;\r\n /** Overlay content (e.g., title text) */\r\n children?: React.ReactNode;\r\n}\r\n\r\nexport function LiquidOcean({\r\n className,\r\n // Original colors - DO NOT CHANGE DEFAULTS\r\n backgroundColor = DEFAULT_THEME.background,\r\n gridColor = DEFAULT_THEME.gridColor,\r\n accentColor = DEFAULT_THEME.accentColor,\r\n // Original settings - DO NOT CHANGE DEFAULTS\r\n fov = 20,\r\n rotationSpeed = 0.001,\r\n showGrid = true,\r\n showBoats = true,\r\n boatCount = 5,\r\n boatSpread = 5,\r\n oceanSize = 25,\r\n oceanFragments = 25,\r\n waveAmplitude = 0.2,\r\n waveSpeed = 0.05,\r\n showWireframe = true,\r\n oceanOpacity = 0.85,\r\n children,\r\n}: LiquidOceanProps) {\r\n const [isVisible, setIsVisible] = React.useState(false);\r\n const containerRef = useRef(null);\r\n\r\n useEffect(() => {\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n setIsVisible(entry.isIntersecting);\r\n },\r\n { threshold: 0 }\r\n );\r\n if (containerRef.current) observer.observe(containerRef.current);\r\n return () => observer.disconnect();\r\n }, []);\r\n\r\n return (\r\n
\r\n \r\n \r\n \r\n\r\n {/* Overlay Content */}\r\n {children && (\r\n
\r\n {children}\r\n
\r\n )}\r\n
\r\n );\r\n}\r\n\r\nexport default LiquidOcean;\r\n", "type": "registry:ui", "target": "components/ui/liquid-ocean.tsx" } ] }