{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "video-player",
  "title": "Video Player",
  "author": "ilinxa",
  "description": "Video element wrapper — autoplay-friendly defaults, slot-based controls, carousel-coordinated pause, and double-tap gestures.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "src/registry/components/media/video-player/video-player.tsx",
      "content": "\"use client\";\n\nimport { memo, useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  DEFAULT_VIDEO_PLAYER_LABELS,\n  type VideoPlayerLabels,\n  type VideoPlayerProps,\n  type VideoState,\n} from \"./types\";\nimport { useVideoState } from \"./hooks/use-video-state\";\nimport { useDoubleTap } from \"./hooks/use-double-tap\";\nimport { VideoElement } from \"./parts/video-element\";\nimport { DefaultControls } from \"./parts/default-controls\";\n\nfunction VideoPlayerInner({\n  src,\n  poster,\n  tracks = [],\n  isActive = true,\n  loop = true,\n  muted = true,\n  playsInline = true,\n  preload = \"metadata\",\n  autoPlay = false,\n  objectFit = \"cover\",\n  controls = true,\n  renderControls,\n  controlsAutoHideMs = 2000,\n  onDoubleTap,\n  onPlay,\n  onPause,\n  onEnded,\n  onTimeUpdate,\n  onLoadedMetadata,\n  onError,\n  labels: labelsProp,\n  className,\n  videoClassName,\n  controlsClassName,\n}: VideoPlayerProps) {\n  const labels = useMemo<Required<VideoPlayerLabels>>(\n    () => ({ ...DEFAULT_VIDEO_PLAYER_LABELS, ...labelsProp }),\n    [labelsProp],\n  );\n\n  const { state, videoRef, togglePlay, toggleMute, videoEventHandlers } =\n    useVideoState({\n      initialMuted: muted,\n      isActive,\n      onPlay,\n      onPause,\n      onEnded,\n      onTimeUpdate,\n      onLoadedMetadata,\n      onError,\n    });\n\n  // (isActive auto-pause now lives inside useVideoState — hook owns video DOM.)\n  const handleDoubleTap = useDoubleTap(onDoubleTap);\n\n  const stateForRender: VideoState = {\n    ...state,\n    isActive,\n    togglePlay,\n    toggleMute,\n  };\n\n  return (\n    <div\n      className={cn(\n        \"relative h-full w-full overflow-hidden bg-black\",\n        className,\n      )}\n      onClick={handleDoubleTap}\n    >\n      <VideoElement\n        videoRef={videoRef}\n        eventHandlers={videoEventHandlers}\n        src={src}\n        poster={poster}\n        tracks={tracks}\n        loop={loop}\n        muted={muted}\n        playsInline={playsInline}\n        preload={preload}\n        autoPlay={autoPlay}\n        objectFit={objectFit}\n        ariaLabel={labels.videoLabel}\n        videoClassName={videoClassName}\n        onTogglePlay={togglePlay}\n        onToggleMute={toggleMute}\n      />\n\n      {controls &&\n        (renderControls ? (\n          renderControls(stateForRender)\n        ) : (\n          <DefaultControls\n            state={stateForRender}\n            labels={labels}\n            controlsAutoHideMs={controlsAutoHideMs}\n            controlsClassName={controlsClassName}\n          />\n        ))}\n    </div>\n  );\n}\n\nconst VideoPlayer = memo(VideoPlayerInner);\nVideoPlayer.displayName = \"VideoPlayer\";\n\nexport { VideoPlayer };\nexport default VideoPlayer;\n",
      "type": "registry:component",
      "target": "components/video-player/video-player.tsx"
    },
    {
      "path": "src/registry/components/media/video-player/index.ts",
      "content": "export { default as VideoPlayer } from \"./video-player\";\n\nexport type {\n  VideoPlayerProps,\n  VideoPlayerLabels,\n  VideoState,\n  VideoTrack,\n} from \"./types\";\n\nexport { DEFAULT_VIDEO_PLAYER_LABELS } from \"./types\";\n\nexport {\n  useDoubleTap,\n  type UseDoubleTapOptions,\n} from \"./hooks/use-double-tap\";\n\n",
      "type": "registry:component",
      "target": "components/video-player/index.ts"
    },
    {
      "path": "src/registry/components/media/video-player/types.ts",
      "content": "import type { ReactNode } from \"react\";\n\nexport interface VideoTrack {\n  /** Track type. */\n  kind: TextTrackKind;\n  /** WebVTT URL. */\n  src: string;\n  /** BCP-47 language tag (e.g. \"en\", \"tr-TR\"). */\n  srcLang: string;\n  /** Human-readable label shown in the browser's track menu. */\n  label: string;\n  /** Mark this track as default (only one per kind should be default). */\n  default?: boolean;\n}\n\nexport interface VideoState {\n  /** Currently playing — mirrors the underlying `<video>` element. */\n  isPlaying: boolean;\n  /** Currently muted. */\n  isMuted: boolean;\n  /** Active state from the `isActive` prop. */\n  isActive: boolean;\n  /** True after `loadedmetadata` fires. `duration` is reliable from this point. */\n  isLoaded: boolean;\n  /** Total duration in seconds (NaN until loaded). */\n  duration: number;\n  /** Current playback position in seconds (rAF-throttled). */\n  currentTime: number;\n  /** Imperative: toggle play/pause. */\n  togglePlay: () => void;\n  /** Imperative: toggle mute/unmute. */\n  toggleMute: () => void;\n}\n\nexport interface VideoPlayerLabels {\n  /** Default: \"Play\". aria-label on the play button. */\n  play?: string;\n  /** Default: \"Pause\". aria-label on the pause button. */\n  pause?: string;\n  /** Default: \"Mute\". aria-label on the mute toggle when audio is on. */\n  mute?: string;\n  /** Default: \"Unmute\". aria-label on the mute toggle when audio is muted. */\n  unmute?: string;\n  /** Default: \"Video\". aria-label on the <video> element itself. */\n  videoLabel?: string;\n}\n\nexport interface VideoPlayerProps {\n  /** Video URL. Required. */\n  src: string;\n  /** Poster image URL. Browser shows it before playback + on load failure. */\n  poster?: string;\n  /** Caption / subtitle tracks. Renders as `<track>` children. */\n  tracks?: VideoTrack[];\n\n  // ─── Active state (carousel coordination) ────────────────────────\n  /** When false, video pauses + state resets. Default: true. */\n  isActive?: boolean;\n\n  // ─── <video> attribute pass-through ──────────────────────────────\n  /** Default: true. */\n  loop?: boolean;\n  /** Default: true (autoplay-friendly; browsers block unmuted autoplay). */\n  muted?: boolean;\n  /** Default: true (iOS Safari requires this for inline playback). */\n  playsInline?: boolean;\n  /** Default: \"metadata\". */\n  preload?: \"none\" | \"metadata\" | \"auto\";\n  /** Default: false. Subject to browser autoplay policy. */\n  autoPlay?: boolean;\n\n  // ─── Visual ──────────────────────────────────────────────────────\n  /** Object-fit for the video element. Default: \"cover\". */\n  objectFit?: \"cover\" | \"contain\";\n\n  // ─── Controls ────────────────────────────────────────────────────\n  /** Show control overlay (default OR custom). Default: true. */\n  controls?: boolean;\n  /** Custom control overlay. Receives full state + dispatchers. */\n  renderControls?: (state: VideoState) => ReactNode;\n  /** Auto-hide controls after N ms during playback. Default: 2000. Pass 0 to disable. */\n  controlsAutoHideMs?: number;\n\n  // ─── Gestures ────────────────────────────────────────────────────\n  /** Fires on a 300ms-window double-tap. Caller wires e.g. heart burst. */\n  onDoubleTap?: () => void;\n\n  // ─── Lifecycle callbacks ─────────────────────────────────────────\n  onPlay?: () => void;\n  onPause?: () => void;\n  onEnded?: () => void;\n  /** Throttled to one call per requestAnimationFrame max. */\n  onTimeUpdate?: (currentTime: number, duration: number) => void;\n  /** Fires once on `loadedmetadata`. */\n  onLoadedMetadata?: (duration: number) => void;\n  /** Fires on video element error. */\n  onError?: (error: MediaError | null) => void;\n\n  // ─── i18n ────────────────────────────────────────────────────────\n  /** Localized labels. Defaults are English. */\n  labels?: VideoPlayerLabels;\n\n  // ─── Style overrides ─────────────────────────────────────────────\n  /** Override classes for the wrapping <div>. */\n  className?: string;\n  /** Override classes for the <video> element. */\n  videoClassName?: string;\n  /** Override classes for the default control overlay. Ignored when `renderControls` is provided. */\n  controlsClassName?: string;\n}\n\n/** Default English labels — exported for consumer composition. */\nexport const DEFAULT_VIDEO_PLAYER_LABELS: Required<VideoPlayerLabels> = {\n  play: \"Play\",\n  pause: \"Pause\",\n  mute: \"Mute\",\n  unmute: \"Unmute\",\n  videoLabel: \"Video\",\n};\n",
      "type": "registry:component",
      "target": "components/video-player/types.ts"
    },
    {
      "path": "src/registry/components/media/video-player/parts/video-element.tsx",
      "content": "\"use client\";\n\nimport type { ReactEventHandler, SyntheticEvent } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type { VideoTrack } from \"../types\";\n\ninterface VideoEventHandlers {\n  onPlay: ReactEventHandler<HTMLVideoElement>;\n  onPause: ReactEventHandler<HTMLVideoElement>;\n  onEnded: ReactEventHandler<HTMLVideoElement>;\n  onLoadStart: ReactEventHandler<HTMLVideoElement>;\n  onLoadedMetadata: ReactEventHandler<HTMLVideoElement>;\n  onTimeUpdate: ReactEventHandler<HTMLVideoElement>;\n  onError: (e: SyntheticEvent<HTMLVideoElement>) => void;\n  onVolumeChange: ReactEventHandler<HTMLVideoElement>;\n}\n\ninterface VideoElementProps {\n  videoRef: (node: HTMLVideoElement | null) => void;\n  eventHandlers: VideoEventHandlers;\n  src: string;\n  poster?: string;\n  tracks: VideoTrack[];\n  loop: boolean;\n  muted: boolean;\n  playsInline: boolean;\n  preload: \"none\" | \"metadata\" | \"auto\";\n  autoPlay: boolean;\n  objectFit: \"cover\" | \"contain\";\n  ariaLabel: string;\n  videoClassName?: string;\n  /** Called when the user clicks the video element (NOT a control button). */\n  onTogglePlay: () => void;\n  /** Called for the M keyboard shortcut. */\n  onToggleMute: () => void;\n}\n\nexport function VideoElement({\n  videoRef,\n  eventHandlers,\n  src,\n  poster,\n  tracks,\n  loop,\n  muted,\n  playsInline,\n  preload,\n  autoPlay,\n  objectFit,\n  ariaLabel,\n  videoClassName,\n  onTogglePlay,\n  onToggleMute,\n}: VideoElementProps) {\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLVideoElement>) => {\n    if (e.key === \" \" || e.code === \"Space\") {\n      e.preventDefault();\n      onTogglePlay();\n    } else if (e.key === \"m\" || e.key === \"M\") {\n      e.preventDefault();\n      onToggleMute();\n    }\n  };\n\n  return (\n    <video\n      ref={videoRef}\n      src={src}\n      poster={poster}\n      loop={loop}\n      muted={muted}\n      playsInline={playsInline}\n      preload={preload}\n      autoPlay={autoPlay}\n      tabIndex={0}\n      aria-label={ariaLabel}\n      onClick={onTogglePlay}\n      onKeyDown={handleKeyDown}\n      {...eventHandlers}\n      className={cn(\n        \"h-full w-full cursor-pointer\",\n        objectFit === \"cover\" ? \"object-cover\" : \"object-contain\",\n        videoClassName,\n      )}\n    >\n      {tracks.map((t, i) => (\n        <track\n          key={`${t.kind}-${t.srcLang}-${i}`}\n          kind={t.kind}\n          src={t.src}\n          srcLang={t.srcLang}\n          label={t.label}\n          default={t.default}\n        />\n      ))}\n    </video>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/video-player/parts/video-element.tsx"
    },
    {
      "path": "src/registry/components/media/video-player/parts/default-controls.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useState, useSyncExternalStore } from \"react\";\nimport { Pause, Play, Volume2, VolumeX } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { VideoPlayerLabels, VideoState } from \"../types\";\n\ninterface DefaultControlsProps {\n  state: VideoState;\n  labels: Required<VideoPlayerLabels>;\n  controlsAutoHideMs: number;\n  controlsClassName?: string;\n}\n\nconst REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\n\nconst subscribeReducedMotion = (callback: () => void) => {\n  if (typeof window === \"undefined\") return () => {};\n  const mql = window.matchMedia(REDUCED_MOTION_QUERY);\n  mql.addEventListener(\"change\", callback);\n  return () => mql.removeEventListener(\"change\", callback);\n};\n\nconst getReducedMotionSnapshot = () =>\n  typeof window === \"undefined\"\n    ? false\n    : window.matchMedia(REDUCED_MOTION_QUERY).matches;\n\nconst getReducedMotionServerSnapshot = () => false;\n\nfunction usePrefersReducedMotion() {\n  return useSyncExternalStore(\n    subscribeReducedMotion,\n    getReducedMotionSnapshot,\n    getReducedMotionServerSnapshot,\n  );\n}\n\nexport function DefaultControls({\n  state,\n  labels,\n  controlsAutoHideMs,\n  controlsClassName,\n}: DefaultControlsProps) {\n  const { isPlaying, isMuted, togglePlay, toggleMute } = state;\n  const reducedMotion = usePrefersReducedMotion();\n\n  // Controls are always visible when paused / auto-hide disabled / reduced-motion.\n  const alwaysVisible =\n    !isPlaying || controlsAutoHideMs <= 0 || reducedMotion;\n\n  // Only used in auto-hide mode. Bumped via `wakeKey` to re-arm the timer.\n  const [hidden, setHidden] = useState(false);\n  const [wakeKey, setWakeKey] = useState(0);\n\n  const showControls = alwaysVisible || !hidden;\n\n  useEffect(() => {\n    if (alwaysVisible) return;\n    const t = setTimeout(() => setHidden(true), controlsAutoHideMs);\n    return () => clearTimeout(t);\n  }, [alwaysVisible, controlsAutoHideMs, wakeKey]);\n\n  const wakeControls = useCallback(() => {\n    setHidden(false);\n    setWakeKey((k) => k + 1);\n  }, []);\n\n  return (\n    <div\n      className={cn(\n        \"pointer-events-none absolute inset-0\",\n        controlsClassName,\n      )}\n      onMouseEnter={wakeControls}\n      onMouseMove={wakeControls}\n    >\n      {/* Big center play button — always visible when paused */}\n      {!isPlaying && (\n        <div className=\"absolute inset-0 flex items-center justify-center\">\n          <Button\n            type=\"button\"\n            variant=\"secondary\"\n            size=\"icon\"\n            className=\"pointer-events-auto h-14 w-14 rounded-full bg-background/80 hover:bg-background/90\"\n            onClick={togglePlay}\n            aria-label={labels.play}\n          >\n            <Play className=\"h-6 w-6 fill-current\" aria-hidden=\"true\" />\n          </Button>\n        </div>\n      )}\n\n      {/* Bottom-right mute toggle — auto-hides during playback */}\n      <div\n        className={cn(\n          \"absolute right-3 bottom-3 flex gap-2 transition-opacity duration-300\",\n          showControls ? \"opacity-100\" : \"opacity-0\",\n        )}\n      >\n        <Button\n          type=\"button\"\n          variant=\"secondary\"\n          size=\"icon\"\n          className=\"pointer-events-auto h-8 w-8 rounded-full bg-background/70 hover:bg-background/90\"\n          onClick={toggleMute}\n          aria-pressed={isMuted}\n          aria-label={isMuted ? labels.unmute : labels.mute}\n        >\n          {isMuted ? (\n            <VolumeX className=\"h-4 w-4\" aria-hidden=\"true\" />\n          ) : (\n            <Volume2 className=\"h-4 w-4\" aria-hidden=\"true\" />\n          )}\n        </Button>\n      </div>\n\n      {/* Bottom-left small pause indicator — only when playing + showing controls */}\n      {isPlaying && showControls && (\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"pointer-events-auto absolute bottom-3 left-3 h-8 w-8 rounded-full bg-background/70 hover:bg-background/90\"\n          onClick={togglePlay}\n          aria-label={labels.pause}\n        >\n          <Pause className=\"h-4 w-4\" aria-hidden=\"true\" />\n        </Button>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/video-player/parts/default-controls.tsx"
    },
    {
      "path": "src/registry/components/media/video-player/hooks/use-video-state.ts",
      "content": "import { useCallback, useEffect, useMemo, useReducer, useRef } from \"react\";\nimport type { VideoState } from \"../types\";\n\ntype VideoStateValue = Pick<\n  VideoState,\n  \"isPlaying\" | \"isMuted\" | \"isLoaded\" | \"duration\" | \"currentTime\"\n>;\n\ntype VideoAction =\n  | { type: \"play\" }\n  | { type: \"pause\" }\n  | { type: \"set-muted\"; muted: boolean }\n  | { type: \"loadstart\" }\n  | { type: \"loaded\"; duration: number }\n  | { type: \"set-current-time\"; currentTime: number }\n  | { type: \"ended\" }\n  | { type: \"reset\" };\n\nconst initialState: VideoStateValue = {\n  isPlaying: false,\n  isMuted: true,\n  isLoaded: false,\n  duration: NaN,\n  currentTime: 0,\n};\n\nfunction videoReducer(\n  state: VideoStateValue,\n  action: VideoAction,\n): VideoStateValue {\n  switch (action.type) {\n    case \"play\":\n      return state.isPlaying ? state : { ...state, isPlaying: true };\n    case \"pause\":\n      return state.isPlaying ? { ...state, isPlaying: false } : state;\n    case \"set-muted\":\n      return state.isMuted === action.muted\n        ? state\n        : { ...state, isMuted: action.muted };\n    case \"loadstart\":\n      return { ...initialState, isMuted: state.isMuted };\n    case \"loaded\":\n      return { ...state, isLoaded: true, duration: action.duration };\n    case \"set-current-time\":\n      return state.currentTime === action.currentTime\n        ? state\n        : { ...state, currentTime: action.currentTime };\n    case \"ended\":\n      return { ...state, isPlaying: false, currentTime: 0 };\n    case \"reset\":\n      return { ...initialState, isMuted: state.isMuted };\n    default:\n      return state;\n  }\n}\n\nexport interface UseVideoStateOptions {\n  initialMuted: boolean;\n  isActive: boolean;\n  onPlay?: () => void;\n  onPause?: () => void;\n  onEnded?: () => void;\n  onTimeUpdate?: (currentTime: number, duration: number) => void;\n  onLoadedMetadata?: (duration: number) => void;\n  onError?: (error: MediaError | null) => void;\n}\n\nexport function useVideoState({\n  initialMuted,\n  isActive,\n  onPlay,\n  onPause,\n  onEnded,\n  onTimeUpdate,\n  onLoadedMetadata,\n  onError,\n}: UseVideoStateOptions) {\n  const [state, dispatch] = useReducer(videoReducer, {\n    ...initialState,\n    isMuted: initialMuted,\n  });\n\n  const videoElRef = useRef<HTMLVideoElement | null>(null);\n  const videoRef = useCallback((node: HTMLVideoElement | null) => {\n    videoElRef.current = node;\n  }, []);\n\n  // Stable refs for callbacks (avoid effect cascade on identity change).\n  const onPlayRef = useRef(onPlay);\n  const onPauseRef = useRef(onPause);\n  const onEndedRef = useRef(onEnded);\n  const onTimeUpdateRef = useRef(onTimeUpdate);\n  const onLoadedMetadataRef = useRef(onLoadedMetadata);\n  const onErrorRef = useRef(onError);\n  useEffect(() => {\n    onPlayRef.current = onPlay;\n    onPauseRef.current = onPause;\n    onEndedRef.current = onEnded;\n    onTimeUpdateRef.current = onTimeUpdate;\n    onLoadedMetadataRef.current = onLoadedMetadata;\n    onErrorRef.current = onError;\n  });\n\n  // rAF cleanup on unmount.\n  const rafRef = useRef<number | null>(null);\n  useEffect(() => {\n    return () => {\n      if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n    };\n  }, []);\n\n  // Pause when isActive flips false. Hook owns video DOM, single dep.\n  useEffect(() => {\n    if (!isActive) videoElRef.current?.pause();\n  }, [isActive]);\n\n  // Memoized handlers — empty deps because all state is in stable refs.\n  const handleVideoPlay = useCallback(() => {\n    dispatch({ type: \"play\" });\n    onPlayRef.current?.();\n  }, []);\n\n  const handleVideoPause = useCallback(() => {\n    dispatch({ type: \"pause\" });\n    onPauseRef.current?.();\n  }, []);\n\n  const handleVideoEnded = useCallback(() => {\n    dispatch({ type: \"ended\" });\n    onEndedRef.current?.();\n  }, []);\n\n  const handleVideoLoadStart = useCallback(() => {\n    dispatch({ type: \"loadstart\" });\n  }, []);\n\n  const handleVideoLoadedMetadata = useCallback(() => {\n    const el = videoElRef.current;\n    if (!el) return;\n    dispatch({ type: \"loaded\", duration: el.duration });\n    onLoadedMetadataRef.current?.(el.duration);\n  }, []);\n\n  const handleVideoTimeUpdate = useCallback(() => {\n    if (rafRef.current !== null) return;\n    rafRef.current = requestAnimationFrame(() => {\n      rafRef.current = null;\n      const el = videoElRef.current;\n      if (!el) return;\n      dispatch({ type: \"set-current-time\", currentTime: el.currentTime });\n      onTimeUpdateRef.current?.(el.currentTime, el.duration);\n    });\n  }, []);\n\n  const handleVideoError = useCallback(\n    (e: React.SyntheticEvent<HTMLVideoElement>) => {\n      const el = e.currentTarget;\n      onErrorRef.current?.(el.error);\n    },\n    [],\n  );\n\n  const handleVideoVolumeChange = useCallback(() => {\n    const el = videoElRef.current;\n    if (!el) return;\n    dispatch({ type: \"set-muted\", muted: el.muted });\n  }, []);\n\n  const videoEventHandlers = useMemo(\n    () => ({\n      onPlay: handleVideoPlay,\n      onPause: handleVideoPause,\n      onEnded: handleVideoEnded,\n      onLoadStart: handleVideoLoadStart,\n      onLoadedMetadata: handleVideoLoadedMetadata,\n      onTimeUpdate: handleVideoTimeUpdate,\n      onError: handleVideoError,\n      onVolumeChange: handleVideoVolumeChange,\n    }),\n    [\n      handleVideoPlay,\n      handleVideoPause,\n      handleVideoEnded,\n      handleVideoLoadStart,\n      handleVideoLoadedMetadata,\n      handleVideoTimeUpdate,\n      handleVideoError,\n      handleVideoVolumeChange,\n    ],\n  );\n\n  const togglePlay = useCallback(() => {\n    const el = videoElRef.current;\n    if (!el) return;\n    if (el.paused) {\n      void el.play().catch(() => {\n        // Browser autoplay policy may reject; user gesture is required.\n      });\n    } else {\n      el.pause();\n    }\n  }, []);\n\n  const toggleMute = useCallback(() => {\n    const el = videoElRef.current;\n    if (!el) return;\n    el.muted = !el.muted;\n    // onVolumeChange event will fire and dispatch set-muted.\n  }, []);\n\n  return { state, videoRef, togglePlay, toggleMute, videoEventHandlers };\n}\n",
      "type": "registry:component",
      "target": "components/video-player/hooks/use-video-state.ts"
    },
    {
      "path": "src/registry/components/media/video-player/hooks/use-double-tap.ts",
      "content": "import { useCallback, useRef } from \"react\";\n\nexport interface UseDoubleTapOptions {\n  /** Time window in ms within which a second tap counts as a double-tap. Default: 300. */\n  windowMs?: number;\n}\n\n/**\n * Returns a click handler that fires `onDoubleTap` only when invoked twice\n * within `windowMs` of each other. Single-tap is silent. After firing, the\n * timestamp resets so a triple-tap doesn't fire again.\n *\n * Reusable outside `<video>` contexts — image double-tap-to-like, card\n * double-tap-to-favorite, etc.\n */\nexport function useDoubleTap(\n  onDoubleTap: (() => void) | undefined,\n  options?: UseDoubleTapOptions,\n): React.MouseEventHandler<HTMLElement> {\n  const lastTapRef = useRef(0);\n  const windowMs = options?.windowMs ?? 300;\n\n  return useCallback(() => {\n    if (!onDoubleTap) return;\n    const now = performance.now();\n    if (now - lastTapRef.current < windowMs) {\n      onDoubleTap();\n      lastTapRef.current = 0;\n    } else {\n      lastTapRef.current = now;\n    }\n  }, [onDoubleTap, windowMs]);\n}\n",
      "type": "registry:component",
      "target": "components/video-player/hooks/use-double-tap.ts"
    }
  ],
  "categories": [
    "media"
  ],
  "type": "registry:block"
}