{
  "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    <svg aria-hidden=\"true\" className=\"hidden\" focusable={false}>\n      <title>Glass Effect Filter</title>\n      <defs>\n        <filter\n          colorInterpolationFilters=\"sRGB\"\n          height=\"200%\"\n          id={id}\n          width=\"200%\"\n          x=\"-50%\"\n          y=\"-50%\"\n        >\n          <feTurbulence\n            baseFrequency=\"0.05 0.05\"\n            numOctaves=\"1\"\n            result=\"turbulence\"\n            seed=\"1\"\n            type=\"fractalNoise\"\n          />\n          <feGaussianBlur\n            in=\"turbulence\"\n            result=\"blurredNoise\"\n            stdDeviation=\"2\"\n          />\n          <feDisplacementMap\n            in=\"SourceGraphic\"\n            in2=\"blurredNoise\"\n            result=\"displaced\"\n            scale={scale}\n            xChannelSelector=\"R\"\n            yChannelSelector=\"B\"\n          />\n          <feGaussianBlur in=\"displaced\" result=\"finalBlur\" stdDeviation=\"4\" />\n          <feComposite in=\"finalBlur\" in2=\"finalBlur\" operator=\"over\" />\n        </filter>\n      </defs>\n    </svg>\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<typeof liquidButtonVariants>;\n\nfunction LiquidButton({\n  className,\n  liquidVariant = \"default\",\n  children,\n  ...props\n}: LiquidButtonProps) {\n  const filterId = React.useId();\n\n  return (\n    <>\n      <Button\n        className={cn(liquidButtonVariants({ liquidVariant }), className)}\n        {...props}\n      >\n        <div\n          className={cn(\n            \"pointer-events-none absolute inset-0 rounded-[inherit]\",\n            GLASS_SHADOW\n          )}\n        />\n        <div\n          className=\"pointer-events-none absolute inset-0 isolate -z-10 overflow-hidden rounded-[inherit]\"\n          style={{ backdropFilter: `url(\"#${filterId}\")` }}\n        />\n        <span className=\"relative z-10\">{children}</span>\n      </Button>\n      <GlassFilter id={filterId} scale={BUTTON_GLASS_FILTER_SCALE} />\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<HTMLDivElement> &\n  VariantProps<typeof liquidGlassCardVariants> & {\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    <Card\n      className={cn(liquidGlassCardVariants({ glassSize }), className)}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"pointer-events-none absolute inset-0 rounded-[inherit]\",\n          GLASS_SHADOW\n        )}\n      />\n\n      {glassEffect && (\n        <>\n          <div\n            className=\"pointer-events-none absolute inset-0 -z-10 overflow-hidden rounded-[inherit]\"\n            style={{ backdropFilter: `url(\"#${filterId}\")` }}\n          />\n          <GlassFilter id={filterId} scale={DEFAULT_GLASS_FILTER_SCALE} />\n        </>\n      )}\n\n      <div className=\"relative z-10\">{children}</div>\n\n      <div className=\"pointer-events-none absolute inset-0 z-20 rounded-[inherit] bg-gradient-to-r from-transparent via-black/5 to-transparent opacity-0 transition-opacity duration-200 ease-out group-hover:opacity-100 motion-reduce:transition-none dark:via-white/5\" />\n    </Card>\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    <div className=\"pointer-events-none flex h-8 w-10 items-end gap-0.5\">\n      {bars.map((bar) => (\n        <div\n          className={cn(\n            \"w-[3px] rounded-sm\",\n            isPlaying && \"animate-bounce-music motion-reduce:animate-none\"\n          )}\n          key={bar.id}\n          style={{\n            height: isPlaying ? undefined : STATIC_BAR_HEIGHT,\n            animationDelay: `${bar.delay}s`,\n            background: \"linear-gradient(to top, #FF2E55, #FF6B88)\",\n          }}\n        />\n      ))}\n    </div>\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<HTMLDivElement>) => {\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<HTMLDivElement>) => {\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        <div className=\"flex justify-between font-medium text-xs text-zinc-500 dark:text-zinc-400\">\n          <span className=\"tabular-nums\">{formatTime(currentTime)}</span>\n          <span className=\"tabular-nums\">{formatTime(totalDuration)}</span>\n        </div>\n        <div\n          aria-label=\"Seek progress bar\"\n          aria-valuemax={totalDuration}\n          aria-valuemin={MIN_TIME}\n          aria-valuenow={currentTime}\n          aria-valuetext={`${formatTime(currentTime)} of ${formatTime(totalDuration)}`}\n          className=\"relative z-10 h-1 w-full cursor-pointer overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-800\"\n          onClick={handleClick}\n          onKeyDown={handleKeyDown}\n          role=\"slider\"\n          tabIndex={0}\n        >\n          <div\n            className=\"h-full bg-gradient-to-r from-[#FF2E55] to-[#FF6B88] transition-all duration-200\"\n            style={{ width: `${progress}%` }}\n          />\n        </div>\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<HTMLDivElement>(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    <div className=\"w-full max-w-sm\" ref={rootRef}>\n      <LiquidGlassCard className=\"gap-3.5 rounded-3xl border border-zinc-200/60 bg-gradient-to-br from-zinc-50 to-zinc-100 p-4 shadow-xl dark:border-zinc-700/60 dark:from-zinc-900 dark:to-black\">\n        <div className=\"flex items-center gap-3\">\n          <div className=\"relative mr-2 mb-4 h-16 w-16 shrink-0 overflow-hidden rounded-2xl bg-gradient-to-br from-pink-400 via-pink-300 to-rose-200 shadow-lg ring-1 ring-black/5 dark:shadow-xl\">\n            <Image\n              alt=\"Album Art for Glow by Echo\"\n              className=\"h-full w-full object-cover\"\n              height={64}\n              sizes=\"64px\"\n              src=\"https://ferf1mheo22r9ira.public.blob.vercel-storage.com/portrait2-x5MjJSaQ9ed0HZrewEhH7TkZwjZ66K.jpeg\"\n              width={64}\n            />\n          </div>\n\n          <div className=\"flex-1 overflow-hidden\">\n            <h3 className=\"overflow-hidden text-ellipsis whitespace-nowrap font-semibold text-lg text-zinc-900 dark:text-white\">\n              Glow\n            </h3>\n            <p className=\"mt-0.5 text-sm text-zinc-600 dark:text-zinc-400\">\n              Echo\n            </p>\n          </div>\n\n          <VolumeBars isPlaying={isPlaying} />\n        </div>\n\n        <div className=\"flex flex-col gap-2\">\n          <ProgressBar\n            currentTime={currentTime}\n            onSeek={handleSeek}\n            totalDuration={TOTAL_DURATION}\n          />\n\n          <div className=\"mt-1 flex items-center justify-between\">\n            <div className=\"flex items-center justify-center gap-2\">\n              <LiquidButton\n                aria-label=\"Previous track\"\n                className=\"h-10 w-10 rounded-full bg-transparent text-zinc-700 transition-colors hover:bg-zinc-200/80 dark:text-zinc-300 dark:hover:bg-zinc-800/80\"\n                size=\"icon\"\n                variant=\"ghost\"\n              >\n                <ArrowLeft className=\"size-4\" />\n              </LiquidButton>\n              <LiquidButton\n                aria-label={isPlaying ? \"Pause\" : \"Play\"}\n                className=\"h-11 w-11 rounded-full bg-transparent text-zinc-700 transition-colors hover:bg-zinc-200/80 dark:text-zinc-300 dark:hover:bg-zinc-800/80\"\n                onClick={handlePlayPause}\n                size=\"icon\"\n                variant=\"ghost\"\n              >\n                {isPlaying ? (\n                  <Pause className=\"size-5\" />\n                ) : (\n                  <Play className=\"size-5\" />\n                )}\n              </LiquidButton>\n              <LiquidButton\n                aria-label=\"Next track\"\n                className=\"h-10 w-10 rounded-full bg-transparent text-zinc-700 transition-colors hover:bg-zinc-200/80 dark:text-zinc-300 dark:hover:bg-zinc-800/80\"\n                size=\"icon\"\n                variant=\"ghost\"\n              >\n                <ArrowRight className=\"size-4\" />\n              </LiquidButton>\n            </div>\n            <LiquidButton\n              aria-label=\"More options\"\n              className=\"h-10 w-10 rounded-full bg-transparent text-zinc-700 transition-colors hover:bg-zinc-200/80 dark:text-zinc-300 dark:hover:bg-zinc-800/80\"\n              size=\"icon\"\n              variant=\"ghost\"\n            >\n              <MoreHorizontal className=\"size-4\" />\n            </LiquidButton>\n          </div>\n        </div>\n      </LiquidGlassCard>\n    </div>\n  );\n}\n\nexport { LiquidButton, LiquidGlassCard };\nexport default NotificationCenter;\n",
      "path": "/components/kokonutui/liquid-glass-card.tsx",
      "target": "components/kokonutui/liquid-glass-card.tsx"
    }
  ]
}