Playback Ghost

Translucent overlay marking where a canvas object was at a previous timestamp during playback. Install Playback Ghost from the VLLNT UI registry with the shadcn CLI.

Report a bug

Translucent overlay marking where a canvas object was at a previous timestamp during playback. Part of the utility family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

Add Playback Ghost to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/playback-ghost.json

Source

"use client"; import type { ComponentPropsWithoutRef, ReactNode } from "react"; import { cn } from "../../lib/utils"; /** * Object kind — drives the ghost glyph. * * @public */ export type PlaybackGhostKind = | "agent" | "artifact" | "input" | "output" | "run" | "task"; const KIND_GLYPH: Record<PlaybackGhostKind, string> = { agent: "◇", artifact: "◌", input: "↘", output: "↗", run: "▶", task: "▢", }; const KIND_LABEL: Record<PlaybackGhostKind, string> = { agent: "Agent", artifact: "Artifact", input: "Input", output: "Output", run: "Run", task: "Task", }; /** * Localizable strings. * * @public */ export type PlaybackGhostLabels = { /** Aria-label override. Defaults to `"Playback ghost"`. */ region?: string; }; const DEFAULT_LABELS = { region: "Playback ghost", } as const satisfies Required<PlaybackGhostLabels>; /** * Props for {@link PlaybackGhost}. * * @public */ export type PlaybackGhostProps = { /** Optional kind glyph + tooltip. */ kind?: PlaybackGhostKind; /** Optional label rendered next to the glyph (e.g. id, run name). */ label?: ReactNode; /** Localizable strings. */ labels?: PlaybackGhostLabels; /** Ghost opacity `0..1`. Defaults to `0.4`. */ opacity?: number; /** Ghost size in pixels. Defaults to `40`. */ size?: number; /** Center X in canvas pixels. */ x: number; /** Center Y in canvas pixels. */ y: number; } & ComponentPropsWithoutRef<"div">; const clamp01 = (v: number): number => { if (v < 0) { return 0; } if (v > 1) { return 1; } return v; }; /** * Translucent overlay marking where a canvas object was at a previous * timestamp during state playback. Renders a kind glyph (and optional * label) at the historical position so the user can compare the * present canvas against earlier state without losing context. * * Pure presentation; the host computes the historical position from the * scrubbed timestamp. The wrapper is `pointer-events: none` so host * gestures pass through. * * @example * ```tsx * <div className="relative h-screen w-screen"> * <Canvas /> * <PlaybackGhost x={420} y={180} kind="run" label="research-2025" /> * </div> * ``` * * @public */ export const PlaybackGhost = ({ ref, ...props }: PlaybackGhostProps & { ref?: React.Ref<HTMLDivElement> }) => { const { className, kind, label, labels, opacity = 0.4, size = 40, x, y, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; const safeOpacity = clamp01(opacity); const safeSize = size < 16 ? 16 : size; const ariaLabel = kind ? `${resolvedLabels.region}: ${KIND_LABEL[kind]}` : resolvedLabels.region; return ( <div aria-label={ariaLabel} className={cn( "pointer-events-none absolute z-10 inline-flex -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-1.5 rounded-md border border-dashed border-border/70 bg-background/40 px-2 py-1 text-xs text-foreground backdrop-blur-sm", className, )} data-playback-ghost data-playback-kind={kind ?? "unknown"} ref={ref} role="img" style={{ left: x, minHeight: safeSize, minWidth: safeSize, opacity: safeOpacity, top: y, }} {...rest} > {kind ? ( <span aria-hidden="true" className="text-muted-foreground"> {KIND_GLYPH[kind]} </span> ) : null} {label ? ( <span className="truncate" data-playback-ghost-label> {label} </span> ) : null} </div> ); }; PlaybackGhost.displayName = "PlaybackGhost";

Stories

Explore every variant and state in the interactive Storybook:

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Dependencies

  • @vllnt/ui@^0.3.0