Todos os componentes

Chat Interface Testimonial

Chat Interface Testimonial é um componente de layout para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.

UI Layouts MIT layout

O que é

Chat Interface Testimonial é um componente de layout para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

npm i motion

Licença

Este componente vem de UI Layouts e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Chat Interface Testimonial

../../packages/blocks/src/testimonial-section/chat-interface.tsx
'use client'

import { cn } from '@/lib/utils'
import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'motion/react'

interface Message {
  id: number
  sender: string
  avatar: string
  text: string
  side: 'left' | 'right'
  time: string
  reactions: string[]
}

const messages: Message[] = [
  {
    id: 1,
    sender: 'Emily Chen',
    avatar:
      'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop',
    text: 'Just integrated the new grid system. Incredible work!',
    side: 'left',
    time: '2:34 PM',
    reactions: ['👏', '🔥'],
  },
  {
    id: 2,
    sender: 'Team Lab',
    avatar: 'TL',
    text: 'So glad you like it Emily! Let us know if you need any help.',
    side: 'right',
    time: '2:35 PM',
    reactions: ['❤️'],
  },
  {
    id: 3,
    sender: 'Markus T.',
    avatar:
      'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop',
    text: 'The documentation is actually readable for once. Thank you.',
    side: 'left',
    time: '2:37 PM',
    reactions: ['😄', '💯'],
  },
  {
    id: 4,
    sender: 'Emily Chen',
    avatar:
      'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop',
    text: 'Btw, the dark mode tokens are perfect.',
    side: 'left',
    time: '2:38 PM',
    reactions: ['✨'],
  },
  {
    id: 5,
    sender: 'Sarah K.',
    avatar:
      'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop',
    text: 'Migration took 10 minutes. Best decision we made this quarter!',
    side: 'left',
    time: '2:40 PM',
    reactions: ['🚀', '💪'],
  },
]

const TypingIndicator = () => (
  <div className="flex gap-1 px-4 py-3">
    {[0, 1, 2].map((i) => (
      <motion.div
        key={i}
        className="size-2 bg-zinc-400 rounded-full"
        animate={{ scale: [1, 1.2, 1], opacity: [0.5, 1, 0.5] }}
        transition={{
          duration: 1,
          repeat: Infinity,
          delay: i * 0.2,
          ease: 'easeInOut',
        }}
      />
    ))}
  </div>
)

export const ChatTestimonials: React.FC = () => {
  const [visibleMessages, setVisibleMessages] = useState<number[]>([])
  const [showTyping, setShowTyping] = useState(false)
  const [activeReactions, setActiveReactions] = useState<Set<number>>(new Set())

  useEffect(() => {
    const showMessages = async () => {
      for (let i = 0; i < messages.length; i++) {
        // Show typing indicator
        setShowTyping(true)
        await new Promise((resolve) => setTimeout(resolve, 800))

        // Hide typing and show message
        setShowTyping(false)
        setVisibleMessages((prev) => [...prev, i])

        // Wait before next message
        await new Promise((resolve) => setTimeout(resolve, 1200))
      }
    }

    showMessages()
  }, [])

  const toggleReaction = (messageId: number) => {
    setActiveReactions((prev) => {
      const newSet = new Set(prev)
      if (newSet.has(messageId)) {
        newSet.delete(messageId)
      } else {
        newSet.add(messageId)
      }
      return newSet
    })
  }

  return (
    <section className="bg-zinc-100 min-h-screen px-4 font-manrope relative">
      <div className="max-w-7xl mx-auto py-16 relative z-10">
        {/* Header */}
        <div className="text-center mb-16">
          <h2 className="text-5xl font-bold tracking-tight text-zinc-900 mb-4">
            Live Support Conversations
          </h2>
          <p className="text-lg text-zinc-600 max-w-3xl mx-auto">
            See real-time interactions between our support team and customers.
            Experience the responsive, helpful communication that sets us apart.
          </p>
        </div>

        {/* Two-column layout */}
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
          {/* Left Content */}
          <div className="space-y-8">
            <motion.div
              initial={{ opacity: 0, x: -20 }}
              animate={{ opacity: 1, x: 0 }}
              transition={{ duration: 0.6 }}
              className="bg-white rounded-2xl p-8 border border-zinc-200 "
            >
              <h3 className="text-2xl font-bold text-zinc-900 mb-4">
                Real Customer Experiences
              </h3>
              <p className="text-zinc-600 mb-6 leading-relaxed">
                Our support team is dedicated to providing exceptional
                assistance. Watch as customers share their genuine experiences
                and receive immediate, helpful responses.
              </p>

              <div className="space-y-4">
                <div className="flex items-center gap-3">
                  <div className="size-12 bg-green-100 rounded-full flex items-center justify-center">
                    <svg
                      className="w-6 h-6 text-green-600"
                      fill="none"
                      stroke="currentColor"
                      viewBox="0 0 24 24"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        strokeWidth={2}
                        d="M13 10V3L4 14h7v7l9-11h-7z"
                      />
                    </svg>
                  </div>
                  <div>
                    <p className="font-semibold text-zinc-900">
                      Lightning Fast Response
                    </p>
                    <p className="text-sm text-zinc-600">
                      Average response time under 2 minutes
                    </p>
                  </div>
                </div>

                <div className="flex items-center gap-3">
                  <div className="size-12 bg-blue-100 rounded-full flex items-center justify-center">
                    <svg
                      className="w-6 h-6 text-blue-600"
                      fill="none"
                      stroke="currentColor"
                      viewBox="0 0 24 24"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        strokeWidth={2}
                        d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"
                      />
                    </svg>
                  </div>
                  <div>
                    <p className="font-semibold text-zinc-900">
                      24/7 Availability
                    </p>
                    <p className="text-sm text-zinc-600">
                      Always here when you need us
                    </p>
                  </div>
                </div>

                <div className="flex items-center gap-3">
                  <div className="size-12 bg-purple-100 rounded-full flex items-center justify-center">
                    <svg
                      className="w-6 h-6 text-purple-600"
                      fill="none"
                      stroke="currentColor"
                      viewBox="0 0 24 24"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        strokeWidth={2}
                        d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
                      />
                    </svg>
                  </div>
                  <div>
                    <p className="font-semibold text-zinc-900">
                      Customer Satisfaction
                    </p>
                    <p className="text-sm text-zinc-600">
                      98% positive feedback rate
                    </p>
                  </div>
                </div>
              </div>
            </motion.div>

            <motion.div
              initial={{ opacity: 0, x: -20 }}
              animate={{ opacity: 1, x: 0 }}
              transition={{ duration: 0.6, delay: 0.2 }}
              className="grid grid-cols-3 gap-4"
            >
              {[
                { label: 'Response Rate', value: '98%' },
                { label: 'Avg. Rating', value: '4.9★' },
                { label: 'Active Users', value: '2.4k' },
              ].map((stat, idx) => (
                <motion.div
                  key={idx}
                  initial={{ opacity: 0, y: 10 }}
                  animate={{ opacity: 1, y: 0 }}
                  transition={{ delay: 0.3 + idx * 0.1 }}
                  className="bg-white border border-zinc-200 rounded-xl p-4 text-center"
                >
                  <p className="text-xl font-bold text-zinc-900">
                    {stat.value}
                  </p>
                  <p className="text-xs text-zinc-500 uppercase tracking-wider">
                    {stat.label}
                  </p>
                </motion.div>
              ))}
            </motion.div>
          </div>

          {/* Right - Chat Interface */}
          <div className="lg:sticky lg:top-8">
            <div className="bg-white rounded-2xl border border-zinc-200  overflow-hidden">
              {/* Chat Header */}
              <div className="bg-zinc-50 px-6 py-4 border-b border-zinc-200">
                <div className="flex items-center justify-between">
                  <div className="flex items-center gap-3">
                    <div className="relative">
                      <div className="size-3 bg-green-500 rounded-full animate-pulse" />
                      <div className="absolute inset-0 size-3 bg-green-500 rounded-full animate-ping opacity-75" />
                    </div>
                    <div>
                      <p className="font-semibold text-zinc-900">
                        Support Chat
                      </p>
                      <p className="text-xs text-zinc-500">
                        42 customers online
                      </p>
                    </div>
                  </div>
                  <div className="flex items-center gap-1">
                    {[1, 2, 3].map((i) => (
                      <div
                        key={i}
                        className="size-1.5 bg-zinc-300 rounded-full"
                      />
                    ))}
                  </div>
                </div>
              </div>

              {/* Messages */}
              <div className="h-96 overflow-y-auto p-5 space-y-3">
                <AnimatePresence>
                  {visibleMessages.map((index) => {
                    const msg = messages[index]
                    const showReactions = activeReactions.has(msg?.id || 0)

                    return (
                      <motion.div
                        key={msg?.id}
                        initial={{ opacity: 0, y: 20, scale: 0.95 }}
                        animate={{ opacity: 1, y: 0, scale: 1 }}
                        transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
                        className={cn(
                          'flex gap-2 max-w-[85%]',
                          msg?.side === 'right'
                            ? 'ml-auto flex-row-reverse'
                            : 'mr-auto'
                        )}
                      >
                        {/* Avatar */}
                        <motion.div
                          initial={{ scale: 0 }}
                          animate={{ scale: 1 }}
                          transition={{
                            delay: 0.2,
                            type: 'spring',
                            stiffness: 500,
                            damping: 25,
                          }}
                          className={cn(
                            'size-8 rounded-full flex items-center justify-center text-xs font-bold shrink-0 overflow-hidden',
                            msg?.side === 'right'
                              ? 'bg-zinc-900 text-white'
                              : 'bg-zinc-200'
                          )}
                        >
                          {msg?.avatar.startsWith('http') ? (
                            <img
                              src={msg.avatar}
                              alt={msg.sender}
                              className="size-full object-cover"
                            />
                          ) : (
                            <span
                              className={
                                msg?.side === 'right'
                                  ? 'text-white'
                                  : 'text-zinc-900'
                              }
                            >
                              {msg?.avatar}
                            </span>
                          )}
                        </motion.div>

                        {/* Message Content */}
                        <div
                          className={cn(
                            'flex flex-col gap-1',
                            msg?.side === 'right' ? 'items-end' : 'items-start'
                          )}
                        >
                          {/* Sender Name */}
                          <span className="text-xs text-zinc-500 font-medium px-1">
                            {msg?.sender}
                          </span>

                          {/* Message Bubble */}
                          <div className="relative group">
                            <motion.div
                              whileHover={{ scale: 1.02 }}
                              className={cn(
                                'px-4 py-2.5 rounded-2xl text-sm cursor-pointer transition-all',
                                msg?.side === 'right'
                                  ? 'bg-zinc-900 text-white rounded-tr-sm'
                                  : 'bg-zinc-50 border border-zinc-200 text-zinc-900 rounded-tl-sm hover:border-zinc-300'
                              )}
                              onClick={() => toggleReaction(msg?.id || 0)}
                            >
                              {msg?.text}
                            </motion.div>

                            {/* Reactions */}
                            <AnimatePresence>
                              {showReactions && (
                                <motion.div
                                  initial={{ opacity: 0, scale: 0.8, y: 5 }}
                                  animate={{ opacity: 1, scale: 1, y: 0 }}
                                  exit={{ opacity: 0, scale: 0.8, y: 5 }}
                                  transition={{ duration: 0.2 }}
                                  className={cn(
                                    'absolute -bottom-3 flex gap-0.5 bg-white border border-zinc-200 rounded-full px-2 py-0.5 ',
                                    msg?.side === 'right' ? 'right-0' : 'left-0'
                                  )}
                                >
                                  {msg?.reactions.map((reaction, idx) => (
                                    <motion.span
                                      key={idx}
                                      initial={{ scale: 0 }}
                                      animate={{ scale: 1 }}
                                      transition={{
                                        delay: idx * 0.05,
                                        type: 'spring',
                                        stiffness: 500,
                                      }}
                                      className="text-xs"
                                    >
                                      {reaction}
                                    </motion.span>
                                  ))}
                                </motion.div>
                              )}
                            </AnimatePresence>
                          </div>

                          {/* Timestamp */}
                          <span className="text-[9px] text-zinc-400 font-medium px-1">
                            {msg?.time}
                          </span>
                        </div>
                      </motion.div>
                    )
                  })}
                </AnimatePresence>

                {/* Typing Indicator */}
                <AnimatePresence>
                  {showTyping && (
                    <motion.div
                      initial={{ opacity: 0, y: 20 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={{ opacity: 0, y: -10 }}
                      transition={{ duration: 0.3 }}
                      className="flex gap-2 max-w-[85%] mr-auto"
                    >
                      <div className="size-8 rounded-full overflow-hidden shrink-0">
                        <img
                          src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop"
                          alt="User typing"
                          className="size-full object-cover"
                        />
                      </div>
                      <div className="bg-zinc-50 border border-zinc-200 rounded-2xl rounded-tl-sm ">
                        <TypingIndicator />
                      </div>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>

              {/* Chat Footer */}
              <div className="bg-zinc-50 px-4 py-2 border-t border-zinc-200">
                <div className="flex items-center gap-3">
                  <input
                    type="text"
                    placeholder="Type a message..."
                    className="flex-1 px-4 py-3 bg-white border border-zinc-200 rounded-full text-sm focus:outline-none focus:border-zinc-300"
                  />
                  <button className="size-10 bg-zinc-900 text-white rounded-full flex items-center justify-center hover:bg-zinc-800 transition-colors">
                    <svg
                      className="w-5 h-5"
                      fill="none"
                      stroke="currentColor"
                      viewBox="0 0 24 24"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        strokeWidth={2}
                        d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
                      />
                    </svg>
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div className="absolute bottom-0 left-0 right-0 top-0 bg-[linear-gradient(to_right,#efefef_1px,transparent_1px),linear-gradient(to_bottom,#efefef_1px,transparent_1px)] bg-size-[6rem_4rem] mask-[radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_110%)]"></div>
      <div className="absolute bottom-0 left-0 right-0 top-0 bg-[radial-gradient(125%_125%_at_50%_10%,rgba(255,255,255,0)_40%,rgba(237,218,50,1)_100%)]"></div>
    </section>
  )
}
./components/ui/timeline-animation.tsx
import type { Variants } from 'motion/react';
import { type HTMLMotionProps, motion, useInView } from 'motion/react';
import type React from 'react';

type TimelineContentProps<T extends keyof HTMLElementTagNameMap> = {
  children?: React.ReactNode;
  animationNum: number;
  className?: string;
  timelineRef: React.RefObject<HTMLElement | null>;
  as?: T;
  customVariants?: Variants;
  once?: boolean;
} & HTMLMotionProps<T>;

export const TimelineAnimation = <T extends keyof HTMLElementTagNameMap = 'div'>({
  children,
  animationNum,
  timelineRef,
  className,
  as,
  customVariants,
  once = true,
  ...props
}: TimelineContentProps<T>) => {
  const defaultSequenceVariants = {
    visible: (i: number) => ({
      filter: 'blur(0px)',
      y: 0,
      opacity: 1,
      transition: {
        delay: i * 0.5,
        duration: 0.5,
      },
    }),
    hidden: {
      filter: 'blur(20px)',
      y: 0,
      opacity: 0,
    },
  };

  const sequenceVariants = customVariants || defaultSequenceVariants;

  const isInView = useInView(timelineRef, {
    once,
  });

  const MotionComponent = motion[as || 'div'] as React.ElementType;

  return (
    <MotionComponent
      initial='hidden'
      animate={isInView ? 'visible' : 'hidden'}
      custom={animationNum}
      variants={sequenceVariants}
      className={className}
      {...props}
    >
      {children}
    </MotionComponent>
  );
};

Componentes parecidos