"use client" /** * LeoThinkingBackdrop — ambient thinking chrome from Leo AI Q2: * gradient blob wash. Cursor-spotlight dots live in `LeoCursorDots` * (Ask Leo panel), not a second AiThinkingOverlay field. * * One geometry for every Leo surface. There used to be a second, `landing`, * for the full-screen route; nothing ever rendered it, and the route shipped * with no wash at all. A variant that only one surface can select is how the * surfaces drift, so the wash is now anchored the same way everywhere: densest * at the foot where the composer is, masked away toward the top. */ import * as React from "react" import { AnimatedBlobBackground, type BlobIntensity, } from "@/components/animated-blob-background" import type { LeoBlobAnimationSpeed } from "@/lib/leo-ambience" import { cn } from "@/lib/utils" /** Cover banner — blobs visible across the strip; strength scales via CSS vars. */ const GREETING_BANNER_BLOB_MASK = [ "linear-gradient(to bottom,", "rgba(0,0,0,0.42) 0%,", "rgba(0,0,0,0.68) 55%,", "#000 88%)", ].join(" ") const GREETING_BANNER_BLOB_MASK_STYLE: React.CSSProperties = { maskImage: GREETING_BANNER_BLOB_MASK, WebkitMaskImage: GREETING_BANNER_BLOB_MASK, } export type LeoThinkingBackdropProps = { /** Show blob layer (disable on very small surfaces if needed). */ blobsEnabled?: boolean /** Faster blob drift while the model is working. */ thinking?: boolean /** `high` on empty hero; `normal` in conversation. */ intensity?: BlobIntensity /** Multiplies the blob layer opacity (prefs slider). */ opacity?: number /** Float / think keyframes. */ animate?: boolean /** * One float / think cycle then settle. Used on What's new Ask Leo cards * so the wash does not keep drifting under the copy. */ playOnce?: boolean animationSpeed?: LeoBlobAnimationSpeed /** Per-blob placement offset (px). Positive X → right, positive Y → down. */ offsetX?: number offsetY?: number /** * `cover` — products-home greeting strip: full-width wash with a gentler top * fade and a strength scale that can push past the composer's 0–1 opacity cap. */ surface?: "composer" | "cover" /** Cover surface only — multiplies `opacity` (same range as launcher strength). */ strengthScale?: number className?: string } /** * Fades the surface's blob layer out toward the top. * * Clipping it instead (`top-1/3` + the wrapper's `overflow-hidden`) cut straight * through three 480px circles, so what should have been the softest part of the * glow became a hard horizontal line with the fully saturated blob interior * sitting right underneath it — the wash read upside down, densest at its top * edge. Masking keeps the geometry intact: the blobs stay anchored to the bottom * where the density belongs, and the top simply stops existing. */ const PANEL_BLOB_MASK = [ "linear-gradient(to bottom,", "transparent 0%,", "rgba(0,0,0,0.10) 42%,", "rgba(0,0,0,0.52) 68%,", "#000 88%)", ].join(" ") /** Thinking: more of the wash is visible so the working state reads from farther up. */ const PANEL_BLOB_MASK_THINKING = [ "linear-gradient(to bottom,", "transparent 0%,", "rgba(0,0,0,0.28) 22%,", "rgba(0,0,0,0.72) 48%,", "#000 72%)", ].join(" ") /** Empty idle (no thread yet): wash reaches the star, not only the composer foot. */ const PANEL_BLOB_MASK_EMPTY = [ "linear-gradient(to bottom,", "transparent 0%,", "rgba(0,0,0,0.18) 28%,", "rgba(0,0,0,0.55) 55%,", "#000 78%)", ].join(" ") const PANEL_BLOB_MASK_STYLE: React.CSSProperties = { maskImage: PANEL_BLOB_MASK, WebkitMaskImage: PANEL_BLOB_MASK, } const PANEL_BLOB_MASK_THINKING_STYLE: React.CSSProperties = { maskImage: PANEL_BLOB_MASK_THINKING, WebkitMaskImage: PANEL_BLOB_MASK_THINKING, } const PANEL_BLOB_MASK_EMPTY_STYLE: React.CSSProperties = { maskImage: PANEL_BLOB_MASK_EMPTY, WebkitMaskImage: PANEL_BLOB_MASK_EMPTY, } export function LeoThinkingBackdrop({ blobsEnabled = true, thinking = false, intensity = "normal", opacity = 1, animate = true, playOnce = false, animationSpeed = "normal", offsetX = 0, offsetY = 0, surface = "composer", strengthScale = 1, className, }: LeoThinkingBackdropProps) { // Empty hero (`high` + idle): fill the surface so the wash sits under the // star. Thread idle stays composer-anchored so it does not stain the // transcript. const emptyHero = !thinking && intensity === "high" const idleBottom = !thinking && !emptyHero && surface === "composer" const coverBanner = surface === "cover" const layerOpacity = coverBanner ? undefined : Math.min(1, Math.max(0, opacity)) if (!blobsEnabled) return null const maskStyle = coverBanner ? GREETING_BANNER_BLOB_MASK_STYLE : thinking ? PANEL_BLOB_MASK_THINKING_STYLE : emptyHero ? PANEL_BLOB_MASK_EMPTY_STYLE : PANEL_BLOB_MASK_STYLE const coverStrengthStyle = coverBanner ? ({ ["--leo-greeting-banner-base-opacity" as string]: opacity, ["--leo-greeting-banner-blob-scale" as string]: strengthScale, } as React.CSSProperties) : undefined return (
) }