{ "name": "liquid-glass-card", "type": "registry:component", "dependencies": [ "lucide-react" ], "registryDependencies": [ "card", "button" ], "files": [ { "type": "registry:component", "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Liquid Glass Card - Optimized with Shadcn UI\n * @version: 2.0.0\n * @date: 2025-10-11\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n ArrowLeft,\n ArrowRight,\n MoreHorizontal,\n Pause,\n Play,\n} from \"lucide-react\";\nimport Image from \"next/image\";\nimport React from \"react\";\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\n\n// Constants for better maintainability\nconst GLASS_SHADOW_LIGHT =\n \"shadow-[0_0_6px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3px_rgba(0,0,0,0.9),inset_-3px_-3px_0.5px_-3px_rgba(0,0,0,0.85),inset_1px_1px_1px_-0.5px_rgba(0,0,0,0.6),inset_-1px_-1px_1px_-0.5px_rgba(0,0,0,0.6),inset_0_0_6px_6px_rgba(0,0,0,0.12),inset_0_0_2px_2px_rgba(0,0,0,0.06),0_0_12px_rgba(255,255,255,0.15)]\";\n\nconst GLASS_SHADOW_DARK =\n \"dark:shadow-[0_0_8px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3.5px_rgba(255,255,255,0.09),inset_-3px_-3px_0.5px_-3.5px_rgba(255,255,255,0.85),inset_1px_1px_1px_-0.5px_rgba(255,255,255,0.6),inset_-1px_-1px_1px_-0.5px_rgba(255,255,255,0.6),inset_0_0_6px_6px_rgba(255,255,255,0.12),inset_0_0_2px_2px_rgba(255,255,255,0.06),0_0_12px_rgba(0,0,0,0.15)]\";\n\nconst GLASS_SHADOW = `${GLASS_SHADOW_LIGHT} ${GLASS_SHADOW_DARK}`;\n\nconst DEFAULT_GLASS_FILTER_SCALE = 30;\nconst BUTTON_GLASS_FILTER_SCALE = 70;\n\n// Shared glass filter component\ninterface GlassFilterProps {\n id: string;\n scale?: number;\n}\n\nconst GlassFilter = React.memo(\n ({ id, scale = DEFAULT_GLASS_FILTER_SCALE }: GlassFilterProps) => (\n \n Glass Effect Filter\n \n \n \n \n \n \n \n \n \n \n )\n);\nGlassFilter.displayName = \"GlassFilter\";\n\n// Liquid Button - extends shadcn Button with glass effect\nconst liquidButtonVariants = cva(\n \"relative transition-transform duration-200 motion-reduce:transition-none\",\n {\n variants: {\n liquidVariant: {\n default:\n \"active:scale-[0.97] motion-reduce:active:scale-100 motion-reduce:hover:scale-100 [@media(hover:hover)]:hover:scale-105\",\n none: \"\",\n },\n },\n defaultVariants: {\n liquidVariant: \"default\",\n },\n }\n);\n\nexport type LiquidButtonProps = ButtonProps &\n VariantProps;\n\nfunction LiquidButton({\n className,\n liquidVariant = \"default\",\n children,\n ...props\n}: LiquidButtonProps) {\n const filterId = React.useId();\n\n return (\n <>\n \n \n \n {children}\n \n \n \n );\n}\n\n// Liquid Glass Card - extends shadcn Card with glass effect\nconst liquidGlassCardVariants = cva(\n \"group relative overflow-hidden bg-background/20 backdrop-blur-[2px]\",\n {\n variants: {\n glassSize: {\n sm: \"p-4\",\n default: \"p-6\",\n lg: \"p-8\",\n },\n },\n defaultVariants: {\n glassSize: \"default\",\n },\n }\n);\n\nexport type LiquidGlassCardProps = React.HTMLAttributes &\n VariantProps & {\n glassEffect?: boolean;\n };\n\nfunction LiquidGlassCard({\n className,\n glassSize,\n glassEffect = true,\n children,\n ...props\n}: LiquidGlassCardProps) {\n const filterId = React.useId();\n\n return (\n \n \n\n {glassEffect && (\n <>\n \n \n \n )}\n\n
{children}
\n\n
\n \n );\n}\n\n// Demo: Music Player Card\nconst TOTAL_DURATION = 45;\nconst VOLUME_BAR_COUNT = 8;\nconst SEEK_JUMP_SECONDS = 5;\nconst TIMER_INTERVAL_MS = 1000;\nconst STATIC_BAR_HEIGHT = \"6px\";\nconst MIN_TIME = 0;\nconst BAR_DELAY_INCREMENT = 0.1;\nconst PROGRESS_PERCENTAGE_MULTIPLIER = 100;\n\nconst formatTime = (timeInSeconds: number): string => {\n const minutes = Math.floor(timeInSeconds / 60);\n const seconds = Math.floor(timeInSeconds % 60);\n return `${minutes}:${seconds.toString().padStart(2, \"0\")}`;\n};\n\ninterface VolumeBarsProps {\n isPlaying: boolean;\n}\n\nconst VolumeBars = React.memo(({ isPlaying }: VolumeBarsProps) => {\n const bars = Array.from({ length: VOLUME_BAR_COUNT }, (_, i) => ({\n id: `bar-${i}`,\n delay: i * BAR_DELAY_INCREMENT,\n }));\n\n return (\n
\n {bars.map((bar) => (\n \n ))}\n
\n );\n});\nVolumeBars.displayName = \"VolumeBars\";\n\ninterface ProgressBarProps {\n currentTime: number;\n totalDuration: number;\n onSeek: (newTime: number) => void;\n}\n\nconst ProgressBar = React.memo(\n ({ currentTime, totalDuration, onSeek }: ProgressBarProps) => {\n const progress =\n (currentTime / totalDuration) * PROGRESS_PERCENTAGE_MULTIPLIER;\n\n const handleClick = (e: React.MouseEvent) => {\n const bar = e.currentTarget;\n const rect = bar.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const percent = x / rect.width;\n const newTime = Math.min(\n Math.max(MIN_TIME, percent * totalDuration),\n totalDuration\n );\n onSeek(newTime);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n switch (e.key) {\n case \"ArrowRight\":\n e.preventDefault();\n onSeek(Math.min(currentTime + SEEK_JUMP_SECONDS, totalDuration));\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n onSeek(Math.max(currentTime - SEEK_JUMP_SECONDS, MIN_TIME));\n break;\n case \"Home\":\n e.preventDefault();\n onSeek(MIN_TIME);\n break;\n case \"End\":\n e.preventDefault();\n onSeek(totalDuration);\n break;\n default:\n break;\n }\n };\n\n return (\n <>\n
\n {formatTime(currentTime)}\n {formatTime(totalDuration)}\n
\n \n \n
\n \n );\n }\n);\nProgressBar.displayName = \"ProgressBar\";\n\nexport function NotificationCenter() {\n const [isPlaying, setIsPlaying] = React.useState(true);\n const [currentTime, setCurrentTime] = React.useState(MIN_TIME);\n const rootRef = React.useRef(null);\n const [isVisible, setIsVisible] = React.useState(true);\n\n // Only tick while on screen — an off-screen interval keeps waking the main\n // thread and re-rendering for a component nobody can see.\n React.useEffect(() => {\n const element = rootRef.current;\n if (!element) {\n return;\n }\n\n const observer = new IntersectionObserver(\n ([entry]) => setIsVisible(entry.isIntersecting),\n { rootMargin: \"100px\" }\n );\n observer.observe(element);\n\n return () => observer.disconnect();\n }, []);\n\n React.useEffect(() => {\n if (!(isPlaying && isVisible)) {\n return;\n }\n\n const intervalId = setInterval(() => {\n setCurrentTime((prev) =>\n prev + 1 >= TOTAL_DURATION ? TOTAL_DURATION : prev + 1\n );\n }, TIMER_INTERVAL_MS);\n\n return () => clearInterval(intervalId);\n }, [isPlaying, isVisible]);\n\n React.useEffect(() => {\n if (currentTime >= TOTAL_DURATION) {\n setIsPlaying(false);\n }\n }, [currentTime]);\n\n const handlePlayPause = () => {\n setIsPlaying((prev) => !prev);\n };\n\n const handleSeek = (newTime: number) => {\n setCurrentTime(newTime);\n if (newTime < TOTAL_DURATION && !isPlaying) {\n setIsPlaying(true);\n }\n };\n\n return (\n
\n \n
\n
\n \n
\n\n
\n

\n Glow\n

\n

\n Echo\n

\n
\n\n \n
\n\n
\n \n\n
\n
\n \n \n \n \n {isPlaying ? (\n \n ) : (\n \n )}\n \n \n \n \n
\n \n \n \n
\n
\n
\n
\n );\n}\n\nexport { LiquidButton, LiquidGlassCard };\nexport default NotificationCenter;\n", "path": "/components/kokonutui/liquid-glass-card.tsx", "target": "components/kokonutui/liquid-glass-card.tsx" } ] }