/** * `useGaze` — the pointer-driven gaze layer (§4.5), as a hook. * * ## Why this is a subpath and not a prop on `` * * A prop would put `blobatar/gaze` in this package's import graph for everyone. * The adapter is 76 B of its own code and the driver is 1.2 kB, so every * consumer rendering static avatars in a list would start paying for a pointer * driver they never run. That is the same trade the rest of the library makes * everywhere it can: expressions are values you import rather than strings you * pass, `motion.css` is a file you choose to load, and `gaze.css` is a second * one. `@blobatar/react-native/animated` is this exact shape already. * * The gate that keeps it honest is `@blobatar/react alone` in * `packages/harness/scripts/size.ts`, budgeted at 110 B. Importing this module * from `index.tsx` would blow it immediately, which is the point. * * A prop would also not have saved you anything. Two of the three steps stay * whatever the API looks like: `import "blobatar/gaze.css"`, and setting * `--mo-track-travel` on the blobatars that should follow the pointer, which is * what opts them in. A prop would have been the third of three while looking * like the only one, and the failure mode of getting it wrong is a face that * renders perfectly and never moves. * * ## What this actually removes * * The ref, the effect, the teardown, and the handle-in-a-ref that any caller * wanting `lookAt` has to keep. That last one is a trap rather than a chore: * the driver holds the eyes' current position, so rebuilding it to change where * it points snaps them to centre. A component that recreated it on each * keystroke would have eyes that jump between every pair of letters. * * ```tsx * import { Blobatar } from "@blobatar/react"; * import { useGaze } from "@blobatar/react/gaze"; * import "blobatar/motion.css"; * import "blobatar/gaze.css"; * * const { ref } = useGaze({ travel: 3 }); * ; * ``` * * `travel` is the excursion, and it is what opts a blobatar in. Leave it out * and the stylesheet owns it instead, which is the better route for a whole * field of blobatars or for anything responsive: * * ```css * .hero .mo-eyes { --mo-track-travel: 3px; } * ``` */ import { type GazeOptions, type GazeTarget } from "blobatar/gaze"; export type { GazeTarget }; /** * The driver's tuning, plus where to look. * * `GazeOptions.target` is not forwarded under its own name, and the difference * is not cosmetic. On the driver it is construction-time sugar: it aims the * driver as it is built and is never read again. A construction-time option * that looks declarative is the classic React trap — passing `target={point}` * and watching it do nothing on the second render — so what this hook takes * instead is `lookAt`, which is applied *whenever it changes* and is therefore * the declarative thing it looks like. */ export type UseGazeOptions = Pick & { /** * Where to look, declaratively. Omit it and the eyes are yours to aim with * the returned `lookAt`. * * This is the whole integration for a page whose answer does not change: * * ```tsx * const { ref } = useGaze({ travel: 3, lookAt: "pointer" }); * ``` * * It takes the same `GazeTarget` union as the function, and it is re-applied * whenever it changes rather than only on mount — including on the render * where the blobatar first appears, so there is no window in which a * declared target has not been asked for yet. A point is compared by its * coordinates rather than by identity, so an inline `{ x, y }` re-aims when * the numbers move and not merely because the object is new. * * **Omitted and `null` are different.** Omitted means "I will aim this * myself", and the hook then never writes over what your `lookAt` calls have * asked for. `null` is a target like any other: look at nothing. A component * that passes `lookAt={cond ? "pointer" : null}` is declaring both, which is * the point of the distinction. * * Do not drive a caret through this. A target that changes on every keystroke * is a render per keystroke to say something the driver could have been told * directly; the function is the seam for that, and the two mix — the last * thing asked for wins, whichever asked. */ lookAt?: GazeTarget; /** * The excursion, in viewBox units. Omit and the stylesheet owns it. * * This is what opts a blobatar into the layer at all: `--mo-track-travel` is * registered with an initial value of `0px`, so a page that loads * `blobatar/gaze.css` and sets it nowhere has a driver running and no eyes * moving. That is the layer's likeliest failure, and its cause is a CSS * property rather than anything visible from here, which is the argument for * the option existing. * * **A rule on `.mo-eyes` beats this, not the other way round.** The hook * writes an inline custom property on the element you attach the ref to, * which is the ``, and the property reaches the eyes by inheriting down * to the `.mo-eyes` group that `gaze.css` reads it on. A selector matching * that group directly — `.something .mo-eyes { --mo-track-travel: … }` — is a * declaration on the element itself, and that always wins over an inherited * value however the ancestor's was written. Inline only outranks a selector * on the *same* element. So do not set both: the rule wins, `travel` does * nothing, and the symptom is a face that renders perfectly and never moves. * Setting it on an ancestor, or on the host the ref is on, is inheritance * against inheritance and the inline value wins there as expected. * * There is no default for the same reason: leave it out and the two can never * collide, because the hook writes nothing. * * CSS keeps three things this cannot do, so it is the better route whenever * one of them applies: setting the excursion on *many* blobatars from one * ancestor, since the property inherits; making it responsive through a media * or container query; and keeping the geometry beside the rest of a * component's geometry. The option is a convenience for the single-blobatar * case, not a replacement for the property. * * The scale: the blobatar is 100 units across, so `3` is 3% of the face, not * three screen pixels. The idle glance's widest stop is a median 1.4 units, * so the range worth trying is about 1.5 to 4, and the ceiling is the eyes * crossing the silhouette — which `motion.css` is explicit is allowed and * reads as a face turning on a round head. */ travel?: number; }; export interface UseGazeResult { /** * Attach to the ``. A callback ref, not an object one. * * It takes the `` of a static blobatar as well as the `` of an * animated one, because `animate` is the caller's to change and a ref that * stopped type-checking when it went off would make toggling animation a * compile error. The layer is simply inert there: an `` has no eyes. * * The driver's life then follows the element's exactly, which an object ref * cannot express: a blobatar that is conditionally rendered, or swapped for * something else, mounts and unmounts without anything in a dependency array * changing, so an effect keyed on options alone would hold a driver pointed * at a detached node. React calls this with the element and with `null`, so * there is nothing to key on and nothing to remember to key on. */ ref: (node: SVGSVGElement | HTMLImageElement | null) => void; /** * Point the eyes at something: a point in client coordinates, an element, * `"pointer"`, `"rest"`, or `null` for nothing. See `GazeTarget`. * * **A blobatar looks at nothing until this is called**, `"pointer"` included: * a driver is armed by mounting and aimed by asking. One line in an effect is * the cursor-following blobatar most pages are after: * * ```tsx * const { ref, lookAt } = useGaze({ travel: 3 }); * useEffect(() => lookAt("pointer"), [lookAt]); * ``` * * A ref works wherever an element does — `lookAt(button.current)` — because * the union takes the element itself and a ref's `.current` is one. There is * deliberately no ref member: a ref is a box React fills in later, and a * driver holding the box rather than what came out of it would be reading * `null` on the first frame after mount and every frame after a swap. * * Stable for the life of the component, so it is safe in a dependency array * and safe to hand to a child — and the effect above is exactly why it has to * be: a `lookAt` that changed identity would re-aim on every render. * * Calling it before the blobatar has mounted is a queued request rather than * a no-op. The target is remembered and handed to the driver the moment there * is one, which is what makes the effect above work at all: it runs a render * before the driver exists, and it never runs again. */ lookAt: (t: GazeTarget) => void; /** * Re-measure the box. * * Scroll, resize and the element's own resizes are already watched. This is * for a host that moved it some other way, or that knows the layout settled * before any observer will say so. */ remeasure: () => void; } export declare function useGaze({ settle, snap, travel, lookAt: declared, }?: UseGazeOptions): UseGazeResult; //# sourceMappingURL=gaze.d.ts.map