{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hls-video-player",
  "type": "registry:component",
  "title": "HLS Video Player",
  "description": "A headless HLS video player with native HLS detection and error handling.",
  "dependencies": [
    "hls.js"
  ],
  "files": [
    {
      "path": "registry/components/hls-video-player.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport type Hls from 'hls.js'\nimport type { ErrorData, ManifestParsedData, LevelSwitchedData } from 'hls.js'\nimport { useComposedRefs } from '@/registry/lib/compose-refs'\n\n/* -------------------------------------------------------------------------------------------------\n * Types\n * -------------------------------------------------------------------------------------------------*/\n\ntype HLSErrorType = 'network' | 'media' | 'fatal' | 'other'\n\nexport interface HLSVideoError {\n  type: HLSErrorType\n  message: string\n  fatal: boolean\n  details?: string\n}\n\nexport interface HLSVideoPlayerProps extends Omit<\n  React.VideoHTMLAttributes<HTMLVideoElement>,\n  'src' | 'width' | 'height' | 'onError'\n> {\n  /** Ref to the underlying video element */\n  ref?: React.Ref<HTMLVideoElement>\n  /** The HLS stream URL (.m3u8) or regular video source */\n  src: string\n  /** Video width (required for aspect ratio) */\n  width: number\n  /** Video height (required for aspect ratio) */\n  height: number\n  /** Enable debug logging */\n  debug?: boolean\n  /** Custom error handler */\n  onHlsError?: (error: HLSVideoError) => void\n  /** Native video error handler */\n  onVideoError?: React.ReactEventHandler<HTMLVideoElement>\n  /** Called when the video is ready to play */\n  onReady?: () => void\n  /** Called when HLS.js library is loaded (only when native HLS not supported) */\n  onHlsLoaded?: () => void\n  /** Start time in seconds */\n  startTime?: number\n  /** Maximum resolution to use (e.g., 720, 1080) */\n  maxResolution?: number\n  /** Minimum resolution to use (e.g., 480, 720) */\n  minResolution?: number\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -------------------------------------------------------------------------------------------------*/\n\nfunction supportsHlsNatively(): boolean {\n  if (typeof window === 'undefined') return false\n  const video = document.createElement('video')\n  return (\n    video.canPlayType('application/vnd.apple.mpegurl') !== '' ||\n    video.canPlayType('audio/mpegurl') !== ''\n  )\n}\n\nfunction isHlsSource(src: string): boolean {\n  return src.includes('.m3u8') || src.includes('application/vnd.apple.mpegurl')\n}\n\nfunction createHlsError(\n  type: HLSErrorType,\n  message: string,\n  fatal: boolean,\n  details?: string\n): HLSVideoError {\n  return { type, message, fatal, details }\n}\n\n/* -------------------------------------------------------------------------------------------------\n * HLSVideoPlayer\n * -------------------------------------------------------------------------------------------------*/\n\nexport function HLSVideoPlayer({\n  ref,\n  src,\n  width,\n  height,\n  debug = false,\n  onHlsError,\n  onVideoError,\n  onReady,\n  onHlsLoaded,\n  startTime,\n  maxResolution,\n  minResolution,\n  autoPlay,\n  ...videoProps\n}: HLSVideoPlayerProps) {\n  const videoRef = React.useRef<HTMLVideoElement>(null)\n  const hlsRef = React.useRef<Hls | null>(null)\n  const [isUsingHls, setIsUsingHls] = React.useState(false)\n\n  const log = React.useCallback(\n    (...args: unknown[]) => {\n      if (debug) {\n        console.log('[HLSVideoPlayer]', ...args)\n      }\n    },\n    [debug]\n  )\n\n  const handleError = React.useCallback(\n    (error: HLSVideoError) => {\n      log('Error:', error)\n      onHlsError?.(error)\n    },\n    [onHlsError, log]\n  )\n\n  const composedRef = useComposedRefs(ref, videoRef)\n\n  // Initialize HLS or native playback\n  React.useEffect(() => {\n    const video = videoRef.current\n    if (!video || !src) return\n\n    let hls: Hls | null = null\n    let destroyed = false\n\n    const setupPlayback = async () => {\n      const isHls = isHlsSource(src)\n      const hasNativeSupport = supportsHlsNatively()\n\n      log('Source:', src)\n      log('Is HLS:', isHls)\n      log('Has native HLS support:', hasNativeSupport)\n\n      // If it's not an HLS source or browser has native support, use native playback\n      if (!isHls || hasNativeSupport) {\n        log('Using native playback')\n        setIsUsingHls(false)\n        video.src = src\n\n        if (startTime && startTime > 0) {\n          video.currentTime = startTime\n        }\n\n        return\n      }\n\n      // Load HLS.js dynamically only when needed\n      try {\n        log('Loading HLS.js...')\n        const HlsModule = await import('hls.js')\n        const HlsClass = HlsModule.default\n\n        if (destroyed) return\n\n        if (!HlsClass.isSupported()) {\n          handleError(\n            createHlsError(\n              'fatal',\n              'HLS is not supported in this browser',\n              true\n            )\n          )\n          return\n        }\n\n        onHlsLoaded?.()\n        log('HLS.js loaded, initializing...')\n\n        hls = new HlsClass({\n          debug,\n          startPosition: startTime ?? -1,\n          capLevelToPlayerSize: true,\n          maxBufferLength: 30,\n          maxMaxBufferLength: 60,\n        })\n\n        hlsRef.current = hls\n        setIsUsingHls(true)\n\n        // Handle HLS events\n        hls.on(\n          HlsClass.Events.MANIFEST_PARSED,\n          (_event, data: ManifestParsedData) => {\n            if (destroyed) return\n            log('Manifest parsed, levels:', data.levels.length)\n\n            // Apply resolution constraints\n            if (maxResolution || minResolution) {\n              const availableLevels = data.levels.map((level, i) => ({\n                height: level.height,\n                width: level.width,\n                bitrate: level.bitrate,\n                index: i,\n              }))\n\n              const validLevels = availableLevels.filter((l) => {\n                if (maxResolution && l.height > maxResolution) return false\n                if (minResolution && l.height < minResolution) return false\n                return true\n              })\n\n              if (validLevels.length > 0 && maxResolution && hls) {\n                // Set to highest valid level\n                const maxLevel = validLevels.reduce((prev, curr) =>\n                  curr.height > prev.height ? curr : prev\n                )\n                hls.currentLevel = maxLevel.index\n              }\n            }\n\n            onReady?.()\n\n            if (autoPlay) {\n              video.play().catch((e) => {\n                log('Autoplay failed:', e)\n              })\n            }\n          }\n        )\n\n        hls.on(\n          HlsClass.Events.LEVEL_SWITCHED,\n          (_event, data: LevelSwitchedData) => {\n            if (destroyed) return\n            log('Level switched to:', data.level)\n          }\n        )\n\n        hls.on(HlsClass.Events.ERROR, (_event, data: ErrorData) => {\n          if (destroyed) return\n          log('HLS error:', data)\n\n          if (data.fatal) {\n            switch (data.type) {\n              case HlsClass.ErrorTypes.NETWORK_ERROR:\n                handleError(\n                  createHlsError(\n                    'network',\n                    'Network error occurred',\n                    true,\n                    data.details\n                  )\n                )\n                // Try to recover\n                hls?.startLoad()\n                break\n              case HlsClass.ErrorTypes.MEDIA_ERROR:\n                handleError(\n                  createHlsError(\n                    'media',\n                    'Media error occurred',\n                    true,\n                    data.details\n                  )\n                )\n                // Try to recover\n                hls?.recoverMediaError()\n                break\n              default:\n                handleError(\n                  createHlsError(\n                    'fatal',\n                    'Fatal error occurred',\n                    true,\n                    data.details\n                  )\n                )\n                hls?.destroy()\n                break\n            }\n          } else {\n            handleError(\n              createHlsError(\n                data.type === HlsClass.ErrorTypes.NETWORK_ERROR\n                  ? 'network'\n                  : data.type === HlsClass.ErrorTypes.MEDIA_ERROR\n                    ? 'media'\n                    : 'other',\n                'Non-fatal error occurred',\n                false,\n                data.details\n              )\n            )\n          }\n        })\n\n        hls.attachMedia(video)\n        hls.loadSource(src)\n      } catch (error) {\n        log('Failed to load HLS.js:', error)\n        handleError(\n          createHlsError(\n            'fatal',\n            'Failed to load HLS library',\n            true,\n            String(error)\n          )\n        )\n      }\n    }\n\n    setupPlayback()\n\n    return () => {\n      destroyed = true\n      if (hls) {\n        log('Destroying HLS instance')\n        hls.destroy()\n        hlsRef.current = null\n      }\n    }\n  }, [\n    src,\n    debug,\n    startTime,\n    maxResolution,\n    minResolution,\n    autoPlay,\n    onReady,\n    onHlsLoaded,\n    handleError,\n    log,\n  ])\n\n  // Handle native video errors\n  const handleVideoError = React.useCallback(\n    (e: React.SyntheticEvent<HTMLVideoElement>) => {\n      const video = e.currentTarget\n      const error = video.error\n\n      if (error) {\n        let errorType: HLSErrorType = 'other'\n        if (error.code === MediaError.MEDIA_ERR_NETWORK) {\n          errorType = 'network'\n        } else if (\n          error.code === MediaError.MEDIA_ERR_DECODE ||\n          error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED\n        ) {\n          errorType = 'media'\n        }\n\n        handleError(\n          createHlsError(errorType, error.message || 'Video error', true)\n        )\n      }\n\n      onVideoError?.(e)\n    },\n    [handleError, onVideoError]\n  )\n\n  const handleCanPlay = React.useCallback(() => {\n    // Only call onReady for native playback\n    if (!isUsingHls) {\n      onReady?.()\n    }\n  }, [isUsingHls, onReady])\n\n  const aspectRatio = width / height\n\n  return (\n    <video\n      ref={composedRef}\n      width={width}\n      height={height}\n      autoPlay={autoPlay}\n      style={{\n        aspectRatio,\n        ...videoProps.style,\n      }}\n      onError={handleVideoError}\n      onCanPlay={handleCanPlay}\n      {...videoProps}\n    />\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/lib/compose-refs.ts",
      "content": "import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n  // eslint-disable-next-line react-hooks/use-memo\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "type": "registry:lib"
    }
  ]
}