Componentmedia1.53 KB gzipZero dependencies

Video Frame

Cinema screening container with sprocket edges and play overlay.

Preview

Live Preview
●MIDNIGHT CINEMA ARCHIVE
[35MM · REEL 04]01:48:22

SCREENING ROOM 01

▶

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add video-frame

Or install the full npm package:

npm install bigbullui
import { VideoFrame } from "@/components/ui/video-frame";

<VideoFrame
  title="CINEMA MATINEE"
  reelNumber="REEL-01"
  duration="01:30:00"
/>

Usage guidance

Use Video Frame when a ticket stub surface needs Cinema screening container with sprocket edges and play overlay. Keep props minimal and prefer composition with sibling stubs.

Avoid Video Frame for long form flows or dense data grids where a dedicated layout block fits better.

Source code

video-frame.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface VideoFrameProps extends React.HTMLAttributes<HTMLDivElement> {
  title?: string;
  reelNumber?: string;
  duration?: string;
  poster?: string;
  src?: string;
  autoPlay?: boolean;
  className?: string;
}

export function VideoFrame({
  title = "MIDNIGHT CINEMA ARCHIVE",
  reelNumber = "REEL-04",
  duration = "02:14:00",
  poster,
  src,
  autoPlay = false,
  className,
  ...props
}: VideoFrameProps) {
  const [isPlaying, setIsPlaying] = React.useState(autoPlay);
  const videoRef = React.useRef<HTMLVideoElement>(null);

  const togglePlay = () => {
    if (videoRef.current) {
      if (isPlaying) {
        videoRef.current.pause();
      } else {
        videoRef.current.play();
      }
      setIsPlaying(!isPlaying);
    } else {
      setIsPlaying(!isPlaying);
    }
  };

  return (
    <div
      className={cn(
        "relative overflow-hidden rounded-xl border-2 border-foreground bg-black shadow-lg font-mono select-none",
        className
      )}
      {...props}
    >
      {/* Top Film Sprocket Strip */}
      <div className="flex h-5 items-center justify-between bg-foreground/90 px-3">
        {Array.from({ length: 12 }).map((_, i) => (
          <div key={`sprocket-top-${i}`} className="h-2.5 w-1.5 rounded-[1px] bg-background/90" />
        ))}
      </div>

      {/* Frame Header */}
      <div className="flex items-center justify-between border-b border-white/20 bg-black/60 px-4 py-2 text-[11px] text-white/80">
        <div className="flex items-center gap-2">
          <span className="text-accent font-bold">●</span>
          <span className="font-bold uppercase tracking-wider">{title}</span>
        </div>
        <div className="flex items-center gap-3 text-[10px] text-white/60">
          <span>[{reelNumber}]</span>
          <span>{duration}</span>
        </div>
      </div>

      {/* Screen Area */}
      <div
        onClick={togglePlay}
        className="group relative flex aspect-video w-full cursor-pointer items-center justify-center bg-zinc-950"
      >
        {src ? (
          <video
            ref={videoRef}
            src={src}
            poster={poster}
            className="size-full object-cover"
            onPlay={() => setIsPlaying(true)}
            onPause={() => setIsPlaying(false)}
          />
        ) : (
          <div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-900/80">
            {/* Visual ticket projector backdrop */}
            <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-zinc-800/40 via-zinc-950/80 to-black" />
            <div className="relative z-10 text-center text-white/50">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-7">
                <rect x="3" y="4.5" width="18" height="15" rx="1.5" />
                <path d="M7.5 4.5v15M16.5 4.5v15" strokeDasharray="2.2 1.8" />
                <path d="M4.5 9.5h3M4.5 14.5h3M16.5 9.5h3M16.5 14.5h3" />
                <circle cx="12" cy="12" r="2.4" />
                <circle cx="12" cy="12" r="4.6" strokeDasharray="2 1.8" />
              </svg>
              <p className="mt-2 text-xs uppercase tracking-widest">SCREENING ROOM 01</p>
            </div>
          </div>
        )}

        {/* Big Play Button Overlay */}
        {!isPlaying && (
          <div className="absolute z-20 flex size-14 items-center justify-center rounded-full border-2 border-accent bg-accent/90 text-white shadow-xl transition-transform group-hover:scale-110 active:scale-95 animate-pulse">
            <span className="ms-1 text-lg font-bold">▶</span>
          </div>
        )}
      </div>

      {/* Bottom Film Sprocket Strip */}
      <div className="flex h-5 items-center justify-between bg-foreground/90 px-3">
        {Array.from({ length: 12 }).map((_, i) => (
          <div key={`sprocket-bot-${i}`} className="h-2.5 w-1.5 rounded-[1px] bg-background/90" />
        ))}
      </div>
    </div>
  );
}

Props

PropTypeDescription
title?string
reelNumber?string
duration?string
poster?string
src?string
autoPlay?boolean
className?string