"use client"; import { motion, useReducedMotion } from "motion/react"; import { type ComponentType, type ReactNode, type SVGProps, useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; export type DropletRingHue = "sky" | "rose" | "emerald" | "violet" | "amber" | "indigo" | "orange" | "teal"; const hueBg: Record = { sky: "bg-sky-500", rose: "bg-rose-500", emerald: "bg-emerald-500", violet: "bg-violet-500", amber: "bg-amber-500", indigo: "bg-indigo-500", orange: "bg-orange-500", teal: "bg-teal-500", }; const hueOrder = Object.keys(hueBg) as DropletRingHue[]; export interface DropletRingApp { name: string; icon: ComponentType>; hue?: DropletRingHue; } export interface DropletRingProps { apps: DropletRingApp[]; mark?: ReactNode; label?: string; hold?: number; className?: string; } const launch = { type: "spring", stiffness: 170, damping: 15, mass: 0.9 } as const; const gather = { type: "spring", stiffness: 210, damping: 26 } as const; const STAGGER = 0.1; function DefaultMark() { return ( ); } function useStageSize() { const ref = useRef(null); const [size, setSize] = useState({ w: 0, h: 0 }); useLayoutEffect(() => { const el = ref.current; if (!el) return; const measure = () => setSize({ w: el.offsetWidth, h: el.offsetHeight }); measure(); const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, []); return [ref, size] as const; } export function DropletRing({ apps, mark, label = "Your product connected to its apps", hold = 5, className, }: DropletRingProps) { const filterId = `droplet-ring-goo-${useId().replace(/:/g, "")}`; const reduce = useReducedMotion(); const [stageRef, size] = useStageSize(); const [out, setOut] = useState(false); const [round, setRound] = useState(0); useEffect(() => { if (reduce) { setOut(true); return; } let timer: ReturnType; const cycle = (next: boolean) => { setOut(next); if (next) setRound((r) => r + 1); if (!next) timer = setTimeout(() => cycle(true), 900); else if (hold > 0) timer = setTimeout(() => cycle(false), hold * 1000 + apps.length * STAGGER * 1000); }; timer = setTimeout(() => cycle(true), 500); return () => clearTimeout(timer); }, [reduce, hold, apps.length]); const radius = Math.min(size.w, size.h) * 0.37; const disc = Math.round(Math.min(size.w, size.h) * 0.3); const drop = Math.round(disc * 0.4); const swell = disc / 2 - drop * 0.3; const ready = size.w > 0; const compact = size.w < 380; const motionFor = (i: number) => { if (reduce) return { duration: 0 }; const delay = out ? i * STAGGER : (apps.length - i) * 0.05; return out ? { ...launch, delay } : { ...gather, delay }; }; const spots = apps.map((_, i) => { const a = -Math.PI / 2 + (i / apps.length) * Math.PI * 2; return { x: Math.cos(a) * radius, y: Math.sin(a) * radius }; }); return (
{ready && ( <>
{apps.map((app, i) => { const Icon = app.icon; const hue = app.hue ?? hueOrder[i % hueOrder.length]; return ( ); })}
{spots.map((spot, i) => { const len = Math.hypot(spot.x, spot.y) || 1; const rim = { x: (spot.x / len) * swell, y: (spot.y / len) * swell }; const still = round === 0 || reduce || !out; return ( ); })}
{mark ?? }
)}
); }