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.
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.jsonSource
"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.