From 15c99f702e0ce5d117fb6e1d03db6d47bc25947e Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 11 Mar 2026 22:42:38 +1300 Subject: [PATCH] feat(quick-25): create useVideoAutoplay hook - Manages video play/pause based on hover and selection state - 300ms hover delay before playing, immediate play on selection - Pauses on mouse leave or deselection (resumes from position) - Catches AbortError from interrupted play() promises --- src/hooks/useVideoAutoplay.ts | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/hooks/useVideoAutoplay.ts diff --git a/src/hooks/useVideoAutoplay.ts b/src/hooks/useVideoAutoplay.ts new file mode 100644 index 00000000..dfcc5c2e --- /dev/null +++ b/src/hooks/useVideoAutoplay.ts @@ -0,0 +1,76 @@ +import { useRef, useEffect } from "react"; +import { useWorkflowStore } from "@/store/workflowStore"; + +/** + * Hook for managing video play/pause based on hover and selection state. + * + * Videos are paused by default. They start playing when: + * - The node is selected (immediately) + * - The node is hovered for 300ms (delayed) + * + * Videos pause when: + * - The node is deselected (if not hovered) + * - The mouse leaves the node (if not selected) + * + * @param nodeId - The node's unique ID + * @param selected - Whether the node is currently selected + * @returns A ref to attach to the video element + */ +export function useVideoAutoplay( + nodeId: string, + selected: boolean | undefined +): React.RefObject { + const videoRef = useRef(null); + const hoverTimeoutRef = useRef | null>(null); + const hoveredNodeId = useWorkflowStore((s) => s.hoveredNodeId); + + const isHovered = hoveredNodeId === nodeId; + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + // Clear any pending hover timeout + const clearHoverTimeout = () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + }; + + if (selected || isHovered) { + // Play the video + if (selected) { + // Selected: play immediately + clearHoverTimeout(); + video.play().catch((e) => { + if (e.name !== "AbortError") { + console.warn("Video play failed:", e); + } + }); + } else if (isHovered) { + // Hovered but not selected: play after 300ms delay + clearHoverTimeout(); + hoverTimeoutRef.current = setTimeout(() => { + video.play().catch((e) => { + if (e.name !== "AbortError") { + console.warn("Video play failed:", e); + } + }); + }, 300); + } + } else { + // Not selected and not hovered: pause + clearHoverTimeout(); + video.pause(); + // Note: Do NOT reset currentTime - resume from current position + } + + // Cleanup on unmount or dependency change + return () => { + clearHoverTimeout(); + }; + }, [isHovered, selected, nodeId]); + + return videoRef; +}