Velocity UI
Loading…
Menu

Ios Liquid Icon Button

Ios Liquid Icon Button component with smooth animations and modern UI.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add ios-liquid-icon-button

Source Code

ios-liquid-icon-button.tsx
'use client'

import React, { useMemo, useState } from 'react'
import { MessageCircle, Camera, LayoutGrid, Plus } from 'lucide-react'

type IconAction = { icon: React.ReactNode; label: string }

export default function OrbitalActionHub({
  actions = [
    { icon: <MessageCircle size={24} strokeWidth={2} />, label: 'Messages' },
    { icon: <Camera size={24} strokeWidth={2} />, label: 'Camera' },
    { icon: <LayoutGrid size={24} strokeWidth={2} />, label: 'Apps' },
  ],
  size = 90,
}) {
  const [isOpen, setIsOpen] = useState(false)
  const filterId = useMemo(() => `liquid-goo-${Math.random().toString(36).slice(2)}`, [])

  const TRAVEL_DISTANCE = size * 1.6
  const ANGLE_SPREAD = 52
  const lerpCurve = 'cubic-bezier(0.23, 1, 0.32, 1)'

  return (
    <div className="relative flex items-center justify-center h-[550px] w-full select-none overflow-hidden">
      {/* 1. ANIMATION SYSTEM */}
      <style>{`
        @keyframes slowSweep {
          from { transform: rotate(0deg); }
          to { transform: rotate(360deg); }
        }
        @keyframes softPulse {
          0%, 100% { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) scale(1); opacity: 0.6; }
          50% { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) scale(1.02); opacity: 0.3; }
        }
        .animate-slow-sweep {
          animation: slowSweep var(--speed) linear infinite;
        }
        .animate-soft-pulse {
          animation: softPulse 4s ease-in-out infinite;
        }
      `}</style>

      {/* 2. DOTTED ORBITS (1px) & BEAMS (0.3px) */}
      <div
        className="absolute transition-all duration-[1200ms] ease-out pointer-events-none"
        style={{
          transform: `scale(${isOpen ? 1.15 : 0.9})`,
          opacity: isOpen ? 0.5 : 0,
        }}
      >
        <svg width="460" height="460" viewBox="0 0 400 400" fill="none">
          <defs>
            {/* Ultra-sharp gradient for the 0.3px beam */}
            <linearGradient id="needle-beam" x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" stopColor="currentColor" stopOpacity="0" />
              <stop offset="95%" stopColor="currentColor" stopOpacity="1" />
              <stop offset="100%" stopColor="currentColor" stopOpacity="0" />
            </linearGradient>
          </defs>

          {[190, 140, 85].map((radius, i) => (
            <g key={radius} className="dark:text-white text-black">
              {/* 1px Dotted Orbit Circumference */}
              <circle
                cx="200"
                cy="200"
                r={radius}
                stroke="currentColor"
                strokeWidth="1"
                strokeDasharray="1 6"
                opacity="0.15"
              />

              {/* 0.3px Precision Beam */}
              <g
                className="animate-slow-sweep"
                style={
                  {
                    transformOrigin: '200px 200px',
                    '--speed': `${18 + i * 10}s`,
                  } as any
                }
              >
                <circle
                  cx="200"
                  cy="200"
                  r={radius}
                  stroke="url(#needle-beam)"
                  strokeWidth="0.3"
                  strokeDasharray="50 350"
                  strokeLinecap="round"
                />
              </g>
            </g>
          ))}
        </svg>
      </div>

      {/* 3. LIQUID FILTER */}
      <svg className="absolute w-0 h-0 invisible" aria-hidden="true">
        <defs>
          <filter id={filterId}>
            <feGaussianBlur in="SourceGraphic" stdDeviation="11" result="blur" />
            <feColorMatrix
              in="blur"
              mode="matrix"
              values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 24 -12"
              result="goo"
            />
            <feComposite in="SourceGraphic" in2="goo" operator="atop" />
          </filter>
        </defs>
      </svg>

      {/* 4. LIQUID BLOB LAYER */}
      <div
        style={{ filter: `url(#${filterId})` }}
        className="absolute inset-0 flex items-center justify-center pointer-events-none"
      >
        {actions.map((_, i) => {
          const angle = (i - (actions.length - 1) / 2) * ANGLE_SPREAD
          const rad = (angle * Math.PI) / 180
          const x = Math.sin(rad) * (isOpen ? TRAVEL_DISTANCE : 0)
          const y = -Math.cos(rad) * (isOpen ? TRAVEL_DISTANCE : 0)

          return (
            <div
              key={`blob-${i}`}
              className="absolute bg-card border border-border rounded-full shadow-sm"
              style={{
                width: size - 12,
                height: size - 12,
                transform: `translate(${x}px, ${y}px) scale(${isOpen ? 1 : 0.4})`,
                transition: `transform 0.8s ${lerpCurve}, opacity 0.6s ease-out`,
                opacity: isOpen ? 1 : 0,
              }}
            />
          )
        })}
        <div
          className="bg-card border border-border rounded-full"
          style={{
            width: size,
            height: size,
            transform: isOpen ? 'scale(0.94)' : 'scale(1)',
            transition: `transform 0.8s ${lerpCurve}`,
          }}
        />
      </div>

      {/* 5. INTERACTIVE CONTENT & PULSING LABELS */}
      <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
        {actions.map((action, i) => {
          const angle = (i - (actions.length - 1) / 2) * ANGLE_SPREAD
          const rad = (angle * Math.PI) / 180
          const x = Math.sin(rad) * (isOpen ? TRAVEL_DISTANCE : 0)
          const y = -Math.cos(rad) * (isOpen ? TRAVEL_DISTANCE : 0)
          const lx = Math.sin(rad) * (isOpen ? TRAVEL_DISTANCE + 85 : 0)
          const ly = -Math.cos(rad) * (isOpen ? TRAVEL_DISTANCE + 85 : 0)

          return (
            <React.Fragment key={`ui-${i}`}>
              <button
                className="absolute flex items-center justify-center pointer-events-auto text-foreground hover:scale-110 active:scale-95 transition-all"
                style={{
                  width: size - 12,
                  height: size - 12,
                  transform: `translate(${x}px, ${y}px) scale(${isOpen ? 1 : 0})`,
                  opacity: isOpen ? 1 : 0,
                  transition: `transform 0.8s ${lerpCurve} ${isOpen ? i * 0.08 : 0}s, opacity 0.4s ease-out`,
                }}
              >
                {action.icon}
              </button>

              <span
                className={`absolute text-[9px] font-black tracking-[0.5em] uppercase text-muted-foreground ${isOpen ? 'animate-soft-pulse' : ''}`}
                style={
                  {
                    '--tw-translate-x': `${lx}px`,
                    '--tw-translate-y': `${ly}px`,
                    transform: `translate(${lx}px, ${ly}px)`,
                    opacity: isOpen ? 0.5 : 0,
                    transition: `opacity 1.2s ${lerpCurve} ${isOpen ? 0.4 + i * 0.08 : 0}s`,
                    animationDelay: `${i * 0.6}s`,
                  } as any
                }
              >
                {action.label}
              </span>
            </React.Fragment>
          )
        })}
      </div>

      {/* 6. MASTER TOGGLE */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="relative z-[100] bg-card border border-border rounded-full flex items-center justify-center active:scale-90 shadow-2xl transition-all duration-500"
        style={{ width: size, height: size }}
      >
        <div
          className="transition-transform duration-[800ms]"
          style={{
            transform: isOpen ? 'rotate(135deg)' : 'rotate(0deg)',
            transitionTimingFunction: lerpCurve,
          }}
        >
          <Plus size={size * 0.35} strokeWidth={2} className="text-foreground" />
        </div>
        <div className="absolute inset-0 rounded-full bg-gradient-to-tr from-transparent via-white/5 to-white/10 pointer-events-none" />
      </button>
    </div>
  )
}

export { OrbitalActionHub as LiquidGlassActionHub }
export { OrbitalActionHub as IOSLiquidMenu }
export { OrbitalActionHub as IOSLiquidIconButton }