"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" 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 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", 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 const layerOpacity = Math.min(1, Math.max(0, opacity)) if (!blobsEnabled) return null const maskStyle = thinking ? PANEL_BLOB_MASK_THINKING_STYLE : emptyHero ? PANEL_BLOB_MASK_EMPTY_STYLE : PANEL_BLOB_MASK_STYLE return (