NEW
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
Learn more
Skip to content
Docs

Video Chat

Video chat in real-time

requires interactionclick
Loading...

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/widget/video-chat.json

Manual

Install dependencies

npm install motion lucide-react

Run the following command

It will create a new file video-chat.tsx inside the components/animata/widget directory.

mkdir -p components/animata/widget && touch components/animata/widget/video-chat.tsx

Paste the code

Open the newly created file and paste the following code:

"use client";
 
import {
  Circle,
  LogOut,
  Maximize,
  Maximize2,
  Mic,
  Monitor,
  Slash,
  Square,
  VideoIcon,
} from "lucide-react";
import { motion } from "motion/react";
import type { StaticImageData } from "next/image";
import { useState } from "react";
 
import { cn } from "@/lib/utils";
import Hills from "@/public/bg-hills.jpg";
import Music from "@/public/widget/music.jpg";
 
interface SquareDivProps {
  bgColor?: string;
  children: React.ReactNode;
  onClick?: () => void;
}
 
interface ImageSlotProps {
  minimize: boolean;
  imageSource: StaticImageData;
  imageAlternate: string;
}
 
interface ControlIconStateProps {
  iconState: {
    Mic: boolean;
    Screen: boolean;
    Video: boolean;
  };
  toggleIconState: (icon: keyof ControlIconStateProps["iconState"]) => void;
}
 
function SquareDiv({ bgColor = "bg-green-600", children, onClick }: SquareDivProps) {
  return (
    <div
      className={cn("flex h-6 w-6 cursor-pointer items-center justify-center rounded-md", bgColor)}
      onClick={onClick}
    >
      {children}
    </div>
  );
}
 
function ImageSlot({ minimize, imageSource, imageAlternate }: ImageSlotProps) {
  return (
    <motion.div
      layout
      transition={{ duration: 0.5 }}
      className={cn(minimize ? "h-48 w-full" : "h-56 w-56")}
    >
      <img
        src={imageSource.src}
        alt={imageAlternate}
        className={cn("h-full w-full object-cover", minimize ? "rounded-xl" : "rounded-3xl")}
      />
    </motion.div>
  );
}
 
function YourImageSlot({ minimize, imageSource, imageAlternate }: ImageSlotProps) {
  return (
    <motion.div
      layout
      transition={{ duration: 0.5 }}
      className={cn(minimize ? "absolute bottom-2 right-0 h-20 w-20" : "h-56 w-56")}
    >
      <img
        src={imageSource.src}
        alt={imageAlternate}
        className={cn("h-full w-full object-cover", minimize ? "rounded-xl" : "rounded-3xl")}
      />
    </motion.div>
  );
}
function ControlIconState({ iconState, toggleIconState }: ControlIconStateProps) {
  return (
    <div className="flex items-center justify-center gap-1">
      <SquareDiv onClick={() => toggleIconState("Mic")}>
        <Mic size={18}>{iconState.Mic && <Slash />}</Mic>
      </SquareDiv>
      <SquareDiv bgColor="bg-slate-400" onClick={() => toggleIconState("Screen")}>
        <Monitor size={18}>{iconState.Screen && <Slash />}</Monitor>
      </SquareDiv>
      <SquareDiv onClick={() => toggleIconState("Video")}>
        <VideoIcon fill="white" size={18}>
          {iconState.Video && <Slash />}
        </VideoIcon>
      </SquareDiv>
      <SquareDiv bgColor="bg-red-600">
        <LogOut size={18} />
      </SquareDiv>
    </div>
  );
}
 
const persons = [
  {
    title: "Ram",
    image: Music,
  },
  {
    title: "Rick",
    image: Hills,
  },
  {
    title: "Alex",
    image: Hills,
  },
];
 
export default function VideoChat() {
  const [minimize, setMinimize] = useState(false);
  const [iconState, setIconState] = useState({
    Mic: false,
    Screen: true,
    Video: false,
  });
  const width = minimize ? "max-w-52" : "max-w-[480px]";
 
  const toggleIconState = (icon: keyof typeof iconState) => {
    setIconState((prevState) => ({
      ...prevState,
      [icon]: !prevState[icon],
    }));
  };
 
  return (
    <motion.div
      layout
      transition={{ duration: 0.5 }}
      className={cn(
        "mx-auto select-none bg-gray-700 text-white",
        width,
        minimize ? "rounded-xl" : "rounded-3xl",
      )}
    >
      <div className="flex items-center justify-between rounded-lg px-4 py-2">
        <div className="flex items-center gap-1">
          <Circle fill="red" stroke="none" size={14} />
          <Circle fill="yellow" stroke="none" size={14} />
          <Circle fill="green" stroke="none" size={14} />
        </div>
        {!minimize && <ControlIconState iconState={iconState} toggleIconState={toggleIconState} />}
        <div className="flex items-center gap-2">
          <button onClick={() => setMinimize(!minimize)}>
            {minimize ? (
              <div className="relative flex">
                <Maximize size={16} />
                <Maximize2 className="absolute" size={16} />
              </div>
            ) : (
              <div className="relative flex">
                <Square size={14} />
                <Square className="absolute -bottom-[1px] -left-[1px]" fill="white" size={10} />
              </div>
            )}
          </button>
          <div className="flex gap-1">
            <Circle fill="white" size={6} />
            <Circle fill="white" size={6} />
            <Circle fill="white" size={6} />
          </div>
        </div>
      </div>
      <motion.div
        layout
        transition={{ duration: 0.5 }}
        className={cn(
          "relative grid grid-cols-1 place-items-center gap-2",
          minimize ? "grid-rows-3" : "grid-cols-2",
        )}
      >
        {persons.map((person, index) => (
          <ImageSlot
            key={index}
            minimize={minimize}
            imageAlternate={person.title}
            imageSource={person.image}
          />
        ))}
        <YourImageSlot minimize={minimize} imageAlternate="Your Name" imageSource={Music} />
      </motion.div>
    </motion.div>
  );
}

Credits

Built by Aashish Katila

Inspired by Video

Photo by Hongru Wang on Unsplash Photo by Jr Korpa on Unsplash