Loomix Player
loomix/loomix-playerFreeComponent
A polished, customizable React video player with play/pause, scrubbable progress, volume, playback speed, captions toggle, picture-in-picture, fullscreen and an optional 'Watch on YouTube' button.
Live preview
live · rendered by the registry
Rendered by loomix on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomix.harshsingh.me/r/loomix-player.jsonSource
1"use client";23import * as React from "react";4import { createPortal } from "react-dom";5import { AnimatePresence, motion } from "motion/react";6import {7 IconBrandYoutubeFilled,8 IconCheck,9 IconMaximize,10 IconMinimize,11 IconPictureInPicture,12 IconPictureInPictureOff,13 IconPlayerPauseFilled,14 IconPlayerPlayFilled,15 IconVolume,16 IconVolume2,17 IconVolume3,18} from "@tabler/icons-react";1920/**21 * A caption / subtitle track to attach to the player.22 */23export type LoomixCaption = {24 src: string;25 srcLang: string;26 label: string;27 default?: boolean;28};2930export type LoomixPlayerProps = {31 /** Video source URL. */32 src: string;33 /** Optional poster image shown before playback. */34 poster?: string;35 /** Optional title rendered in the top-left of the player chrome. */36 title?: string;37 /** Optional YouTube URL; when set, a "Watch on YouTube" button appears in the top-right. */38 youtubeUrl?: string;39 /** Optional close handler; when set, an X button appears in the top-right. */40 onClose?: () => void;41 /** Optional caption / subtitle tracks. */42 captions?: LoomixCaption[];43 /** Optional accessible label for the player. */44 ariaLabel?: string;45 /** Auto-play on mount. Defaults to false. */46 autoPlay?: boolean;47 /** Move keyboard focus to the player on mount so its shortcuts work immediately. Defaults to false. */48 autoFocus?: boolean;49 /** Start muted. Defaults to false. */50 muted?: boolean;51 /** Loop video. Defaults to false. */52 loop?: boolean;53 /** Disable picture-in-picture button. */54 disablePictureInPicture?: boolean;55 /** Class name applied to the player root. */56 className?: string;57 /** Class name applied to the underlying <video> element. */58 videoClassName?: string;59 /** Called whenever play / pause state changes. */60 onPlayingChange?: (isPlaying: boolean) => void;61};6263const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] as const;64type Speed = (typeof SPEEDS)[number];6566const EASE = [0.23, 1, 0.32, 1] as const;6768function formatTime(seconds: number): string {69 if (!Number.isFinite(seconds) || seconds < 0) return "0:00";70 const total = Math.floor(seconds);71 const h = Math.floor(total / 3600);72 const m = Math.floor((total % 3600) / 60);73 const s = total % 60;74 const mm = h > 0 ? String(m).padStart(2, "0") : String(m);75 const ss = String(s).padStart(2, "0");76 return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;77}7879function cn(...values: Array<string | false | null | undefined>): string {80// … truncated
What it pulls in
npm packages
