import type { ComponentPropsWithoutRef, ReactNode } from "react"; import { Fragment, createContext, useCallback, useContext, useRef, useState } from "react"; import { inverseLerp, lerp } from "~/utils/lerp"; interface MousePosition { x: number; y: number; } const MousePositionContext = createContext(undefined); export function MousePositionProvider({ children }: { children: ReactNode }) { const ref = useRef(null); const [position, setPosition] = useState(undefined); const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (!ref.current) { setPosition(undefined); return; } const { top, left, width, height } = ref.current.getBoundingClientRect(); const x = (e.clientX - left) / width; const y = (e.clientY - top) / height; if (x < 0 || x > 1 || y < 0 || y > 1) { setPosition(undefined); return; } setPosition({ x, y }); }, [ref.current] ); return (
setPosition(undefined)} onMouseMove={handleMouseMove} style={{ width: "100%", height: "100%" }} > {children}
); } export const useMousePosition = () => { return useContext(MousePositionContext); }; type TimelineContextState = { startMs: number; durationMs: number; }; const TimelineContext = createContext({} as TimelineContextState); function useTimeline() { return useContext(TimelineContext); } type TimelineMousePositionContextState = { x: number; y: number } | undefined; const _TimelineMousePositionContext = createContext(undefined); export type RootProps = { /** If the timeline doesn't start at zero. Doesn't impact layout but gives you the times back */ startMs?: number; durationMs: number; /** A number between 0 and 1, determines the width between min and max */ scale: number; minWidth: number; maxWidth: number; children?: ReactNode; className?: string; }; /** The main element that determines the dimensions for all sub-elements */ export function Root({ startMs = 0, durationMs, scale, minWidth, maxWidth, children, className, }: RootProps) { const pixelWidth = calculatePixelWidth(minWidth, maxWidth, scale); return (
{children}
); } export type RowProps = ComponentPropsWithoutRef<"div">; /** This simply acts as a container, with position relative. * This allows you to nest "Rows" and put heights on them */ export function Row({ className, children, ...props }: RowProps) { return (
{children}
); } export type PointProps = { ms: number; className?: string; children?: (ms: number) => ReactNode; }; /** A point in time, it has no duration */ export function Point({ ms, className, children }: PointProps) { const { startMs, durationMs } = useTimeline(); const position = inverseLerp(startMs, startMs + durationMs, ms); return (
{children && children(ms)}
); } export type SpanProps = { startMs: number; durationMs: number; className?: string; children?: ReactNode; }; /** As span of time with a start and duration */ export function Span({ startMs, durationMs, className, children }: SpanProps) { const { startMs: rootStartMs, durationMs: rootDurationMs } = useTimeline(); const position = inverseLerp(rootStartMs, rootStartMs + rootDurationMs, startMs); const width = inverseLerp(rootStartMs, rootStartMs + rootDurationMs, startMs + durationMs) - position; return (
{children}
); } export type EquallyDistributeProps = { count: number; children: (ms: number, index: number) => ReactNode; }; /** Render a child equally distributed across the duration */ export function EquallyDistribute({ count, children }: EquallyDistributeProps) { const { startMs, durationMs } = useTimeline(); return ( <> {Array.from({ length: count }).map((_, index) => { const ms = startMs + (durationMs / (count - 1)) * index; return {children(ms, index)}; })} ); } export type FollowCursorProps = { children: (ms: number) => ReactNode; }; /** Renders a child that follows the cursor */ export function FollowCursor({ children }: FollowCursorProps) { const { startMs, durationMs } = useTimeline(); const relativeMousePosition = useMousePosition(); const ms = relativeMousePosition?.x ? lerp(startMs, startMs + durationMs, relativeMousePosition.x) : undefined; if (ms === undefined) return null; return (
{children(ms)}
); } /** Gives the total width of the root */ function calculatePixelWidth(minWidth: number, maxWidth: number, scale: number) { return lerp(minWidth, maxWidth, scale); }