"use client" /** * The wash inside the utility bar's Ask Leo chip — the same drifting bar lobes * the Leo Assist search bar uses, clipped to the chip. * * It replaces the single travelling gradient the launcher used to carry * (`--ask-leo-chip-wash`). That gradient existed because three blurred lobes * sized for a composer pill are each taller than a 32px chip, and clipping them * to the chip only ever shows one lobe's flat middle. `fit="chip"` scales the * lobes to the control instead, so each one's drift crosses the chip top to * bottom the way it crosses the composer. * * Three deliberate departures from `LeoSearchBarWash`: * * - **No dot overlay.** `searchBarDots` draws a 9px grid, which behind a 96px * label is texture on top of type rather than atmosphere behind a text field. * - **Its own placement range.** The composer's pad moves its lobes by up to * 120px, which is wider than this chip; `LEO_LAUNCHER_OFFSET_MAX` is 14, so * the pad can nudge the field without parking it off the control. * - **Quieter at rest.** The field is always on, so its resting opacity is * capped by `--ask-leo-chip-blob-opacity`; the sheen and the high-intensity * fills are held back for the states where Leo is actually working. * * Two layers, not one. The field is clipped to the chip, and a second copy is * masked down to a ring straddling the border: the same lobes, blurred, so the * outline lights where a lobe reaches it and a soft bloom carries just outside * the box. That is the part of the field a clip has to throw away, and putting * it back is what makes the chip read as holding light rather than as a window * cut into it. Both copies mount together and share the drift keyframes, so the * bloom is always under the lobe that caused it. * * It also reads its own prefs (`launcherWash*`) rather than the search bar's, * because the chip lives in the shell's utility bar on every route while the * composer only exists inside Leo. Motion stays shared: speed and the global * blob switch are one decision for all of Leo's surfaces. * * The layer classes are the ones the bar density needs: `leo-ai-blob-layer--bar` * is where the three lobe fills are defined, so dropping it leaves the blobs * unpainted. */ import * as React from "react" import { useReducedMotion } from "motion/react" import { AnimatedBlobBackground } from "../ui/animated-blob-background" import { useLeoAmbience } from "../ui/leo-ambience-context" import { cn } from "../../lib/utils" export function AskLeoLauncherWash({ introActive, open = false, busy = false, className, }: { /** True while the launcher's arrival is playing. */ introActive: boolean /** * Leo's own state, passed in rather than read from a context. * * The wash lives in the package and Leo's thread state does not: which * conversation is open, and whether it is mid-answer, is the consuming app's * to model. Reading a context here would make the package depend on a * provider it cannot ship, so the two states it needs arrive as props and * default to the resting field. */ open?: boolean busy?: boolean className?: string }) { const { prefs, previewThinking } = useLeoAmbience() const reduceMotion = useReducedMotion() ?? false /* Motion and nothing else, so it does not render under reduced motion — the bar lobes hold at full opacity when their keyframes are stripped, and a static brand field parked behind the label is worse than no wash at all. Forced colors is handled in CSS on the clip layer. */ if (reduceMotion || !prefs.launcherWash) return null /* The wash runs whenever the chip is on screen, the way it does in the Leo Assist bar: it is the launcher's resting surface, not a state indicator. Gating it to arrival / open / busy left the chip flat for almost the whole session, so the field only ever appeared as a flash on load and read as a glitch rather than as Leo's own material. What still separates the states is loudness, and that lives in the fills: `--ask-leo-chip-blob-opacity` holds the resting field well under the composer's, and `intensity` lifts it when Leo is working. */ const emphatic = introActive || open || busy || previewThinking /* `high` is a working state, not a resting one: the loudest fills behind a 96px label are legible for the seconds Leo is answering and tiring for the hours it is not. Quieter than `normal` is the strength pref's job. */ const intensity = emphatic && prefs.launcherWashIntensity === "high" ? "high" : "normal" /* Drift only while Leo is visibly live. The wash stays mounted at rest — it is the chip's material — but six blurred lobes animating forever (field + edge spill) is what Chrome's tab-throttle dialog is reacting to. Opacity still lifts on hover/focus via CSS; motion waits for emphatic states. */ const animateWash = prefs.blobAnimations && emphatic /* One configuration, two mounts. Same size, same drift, same moment, so the ring copy below is under its own lobe by construction. */ const fieldProps = { enabled: prefs.thinkingBlob, thinking: true, intensity, density: "bar", fit: "chip", animate: animateWash, animationSpeed: prefs.blobAnimationSpeed, offsetX: prefs.launcherWashOffsetX, offsetY: prefs.launcherWashOffsetY, } as const const strength = { "--ask-leo-chip-blob-scale": prefs.launcherWashStrength, } as React.CSSProperties return ( <> {/* The edge light. Geometry, masking, and blur are in CSS on the two attributes (`globals.css`), including the `@supports` gate that keeps this copy from painting across the chip's face where `mask-composite` is missing. Two nodes because the order matters: the inner one masks the field down to a ring on the border, the outer one blurs the result, so both edges of that ring come out soft. Blurring first and masking after would cut the glow off with a hard rounded-rect edge, which is the seam the whole effect exists to avoid. `z-[2]` puts the ring over the chip, which sits at `z-index: 1`. Under it the light landed beneath a border the chip paints at a flat `brand/45`, so the outline stayed the same colour all the way round however the lobes moved. The mask keeps this copy off the face, so painting it on top costs the label nothing. `lobesOnly` for the same reason: the field's connective floor is even across the width, and read through an 8px band that is an evenly lit outline again. */} ) }