"use client" import * as React from "react" import { useReducedMotion } from "motion/react" import { useTheme } from "../../hooks/use-color-scheme" import { usePageVisible } from "../../hooks/use-page-visible" import { LEO_BLOB_ANIM_RATE, type LeoBlobAnimationSpeed, } from "../../lib/leo-ambience" import { cn } from "../../lib/utils" export type BlobIntensity = "high" | "normal" export type AnimatedBlobBackgroundProps = { className?: string intensity?: BlobIntensity /** When false, unmounts entirely (saves GPU while Leo is hidden). */ enabled?: boolean /** When true, blobs animate faster (thinking / analyzing). */ thinking?: boolean /** Float / think keyframes. Honours prefers-reduced-motion. */ animate?: boolean /** * Run the entrance spread + one gentle float cycle, then settle. * Used on short-lived surfaces (What's new Leo cards) where an infinite * drift would keep competing with the copy. */ playOnce?: boolean animationSpeed?: LeoBlobAnimationSpeed /** * `bar` — compact wash sized for a search / composer pill (blobs live inside * the rounded shell). `default` — full-panel hero / rail wash. */ density?: "default" | "bar" /** * Bar density only. `pill` sizes the lobes for a composer / search pill. * `chip` sizes them for a control roughly one line tall (the utility bar's * Ask Leo chip): the lobes scale to the box instead of being clipped by it, so * each one's drift crosses the control top to bottom rather than showing a * flat middle band of a much taller lobe. */ fit?: "pill" | "chip" /** Soft sheen sweep (bar density + thinking only). Default true. */ sheen?: boolean /** * Bar density. Drops the connective floor, tightens each lobe's blur, and skips * the sheen, so the field reads as three separate hot spots instead of one * continuous wash. * * For consumers that mask the field down to a thin band — the Ask Leo chip's * edge light masks it to a ring on the border — the floor is what ruins the * effect: it spans the full width at a flat opacity, so every part of the band * gets the same light and the outline comes out evenly lit no matter where the * lobes are. */ lobesOnly?: boolean /** * Free placement (px). Bar density: each lobe shell. Panel density: shifts the * whole three-blob field together so composer / cover washes share the same pad. */ offsetX?: number offsetY?: number } /** * Bar lobes are sized and centred as a share of the pill width so they keep the * same overlap at any pill size. Neighbouring lobes overlap by ~20% of the pill * — enough that the wash reads as one field with three hot spots rather than * three detached circles. */ const BAR_LOBES = [ { slot: 1 as const, centerPct: 24, widthPct: 54, heightClass: "h-32" }, { slot: 3 as const, centerPct: 50, widthPct: 46, heightClass: "h-24" }, { slot: 2 as const, centerPct: 76, widthPct: 52, heightClass: "h-28" }, ] /** * Same three slots, heights as a share of the control instead of fixed pixels. * A lobe taller than the control is what makes drift legible — the vertical * keyframes move each lobe by ~10-14% of its own height, and a lobe the exact * height of the box would leave a bald edge at the extremes. Between 1.2x and * 1.6x the box, that travel stays inside the fill while still reading as motion * from top to bottom, which pixel heights against a 32px chip cannot: at `h-32` * the chip only ever shows the lobe's flat middle. */ const CHIP_LOBES = [ { slot: 1 as const, centerPct: 22, widthPct: 50, heightClass: "h-[160%]" }, { slot: 3 as const, centerPct: 50, widthPct: 42, heightClass: "h-[125%]" }, { slot: 2 as const, centerPct: 78, widthPct: 48, heightClass: "h-[145%]" }, ] function BarBlob({ slot, centerPct, widthPct, heightClass, offsetX, offsetY, opacity, blurClass, }: { slot: 1 | 2 | 3 centerPct: number widthPct: number heightClass: string offsetX: number offsetY: number opacity: number blurClass: string }) { // Placement on the position shell; keyframes run on the core (transform). const positionStyle: React.CSSProperties = { opacity, top: "50%", left: `${centerPct}%`, width: `${widthPct}%`, transform: `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px))`, } return (