react-dvd-screensaver
Version:
DVD-era nostalgia in React. Zero dependencies, fully typed, SSR-safe, supports React 16 through 19.
115 lines (113 loc) • 4.15 kB
TypeScript
/**
* Configuration options for the DVD screensaver behavior.
*/
type Options = {
/** Whether the animation should pause when the element is hovered or touched. */
freezeOnHover?: boolean;
/** Callback triggered each time the element hits a container boundary. Receives the total impact count. */
impactCallback?: (count: number) => void;
/** Callback triggered when the element simultaneously hits two boundaries (a corner). */
onCornerHit?: () => void;
/** Callback triggered when hover or touch begins on the element. */
hoverCallback?: () => void;
/** Pause the animation programmatically. */
paused?: boolean;
/**
* Speed of the animation expressed as pixels per frame at 60 fps. Defaults to 2.
* Internally the animation is time-based so the speed stays consistent across
* 60 Hz, 90 Hz, and 120 Hz displays.
*/
speed?: number;
};
/**
* The return type of the useDvdScreensaver hook.
*/
type UseDvdScreensaver<T extends HTMLElement = HTMLDivElement> = {
/** Attach to the container element. Takes precedence over the element's parent when provided. */
containerRef: React.RefObject<T | null>;
/** Attach to the element that should animate. */
elementRef: React.RefObject<T | null>;
/** Whether the element is currently hovered or touched. */
hovered: boolean;
/** Total number of boundary impacts. */
impactCount: number;
};
/**
* Hook that applies a DVD screensaver bouncing animation to an element.
* Works on both desktop (mouse) and mobile (touch) devices, and is safe
* to use in Next.js client components.
*
* The container must have `position: relative` (or `absolute` / `fixed`).
* The animated element must have `position: absolute; top: 0; left: 0`.
* If `containerRef` is not attached to a DOM node, the element's `parentElement` is used.
*
* @param options - Configuration options.
* @returns Refs for the container and element, hover state, and impact count.
*
* @example Basic usage
* ```tsx
* const { containerRef, elementRef, impactCount } = useDvdScreensaver({ speed: 3 });
*
* return (
* <div ref={containerRef} style={{ position: 'relative', width: '100%', height: '400px' }}>
* <div ref={elementRef} style={{ position: 'absolute', top: 0, left: 0 }}>
* <MyLogo />
* </div>
* </div>
* );
* ```
*
* @example Change color on every boundary impact
* ```tsx
* const COLORS = ['#ff4081', '#7c4dff', '#00bcd4', '#69f0ae'];
* const [color, setColor] = useState(COLORS[0]);
*
* const { containerRef, elementRef } = useDvdScreensaver({
* impactCallback: (count) => setColor(COLORS[count % COLORS.length]),
* });
* ```
*
* @example Programmatic pause and resume
* ```tsx
* const [paused, setPaused] = useState(false);
* const { containerRef, elementRef } = useDvdScreensaver({ paused });
*
* return (
* <>
* <div ref={containerRef} style={{ position: 'relative', width: '100%', height: '400px' }}>
* <div ref={elementRef} style={{ position: 'absolute', top: 0, left: 0 }}>
* <MyLogo />
* </div>
* </div>
* <button onClick={() => setPaused((p) => !p)}>{paused ? 'Resume' : 'Pause'}</button>
* </>
* );
* ```
*
* @example Corner hit detection
* ```tsx
* const { containerRef, elementRef } = useDvdScreensaver({
* onCornerHit: () => console.log('Corner!'),
* });
* ```
*
* @example Next.js App Router — add 'use client' directive
* ```tsx
* 'use client';
* import { useDvdScreensaver } from 'react-dvd-screensaver';
*
* export default function ScreensaverPage() {
* const { containerRef, elementRef } = useDvdScreensaver({ speed: 3 });
*
* return (
* <div ref={containerRef} style={{ position: 'relative', width: '100%', height: '100vh' }}>
* <div ref={elementRef} style={{ position: 'absolute', top: 0, left: 0 }}>
* <MyLogo />
* </div>
* </div>
* );
* }
* ```
*/
declare function useDvdScreensaver<T extends HTMLElement = HTMLDivElement>(options?: Partial<Options>): UseDvdScreensaver<T>;
export { type Options, type UseDvdScreensaver, useDvdScreensaver };