{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "slug": "focus-testimonials",
  "name": "Focus Testimonials",
  "title": "Focus Testimonials",
  "type": "registry:ui",
  "description": "Fluid paragraph testimonials featuring inline avatars, cursor-tracking spring physics author tooltip, and dynamic selective blur focus on hover.",
  "summary": "An editorial testimonial component that arranges quotes into a continuous flowing paragraph with inline micro-avatars. On hover, the active testimonial blooms with high contrast and full saturation while all other testimonials gracefully blur and fade. An interactive author tooltip glides above the cursor powered by sub-pixel spring physics.",
  "category": "cards",
  "tags": [
    "testimonials",
    "feedback",
    "social-proof",
    "motion",
    "spring",
    "blur",
    "cursor",
    "tooltip"
  ],
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge",
    "lucide-react"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-01",
  "updatedDate": "2026-10-01",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Natural inline paragraph flow blending quotes and avatars into a seamless typography experience",
    "Selective blur (1.5px on text, 0.6px on avatars) and 25% opacity on non-hovered testimonials",
    "Floating cursor-tracking author card driven by an overdamped spring (stiffness 340, damping 28, mass 0.4)",
    "Smooth expandable reveal with animated chevron toggle for additional social proof",
    "Fully synchronized for both dark and light mode color spaces",
    "High-performance cubic-bezier transitions on hover enter and exit"
  ],
  "anatomy": [
    "<FocusTestimonials> (Main responsive layout wrapper)",
    "<TestimonialSpanItem> (Inline quote unit housing micro-avatar and quote text)",
    "<motion.div> (Floating author tooltip with avatar, name, and role/company)",
    "<button> (Expandable toggle with spring rotating chevron)"
  ],
  "physics": {
    "engine": "Motion Spring & Cubic-Bezier Filters",
    "description": "Mouse-following floating tooltip powered by custom spring physics and CSS cubic-bezier filter transitions.",
    "parameters": [
      {
        "label": "Spring Stiffness",
        "value": "340"
      },
      {
        "label": "Spring Damping",
        "value": "28"
      },
      {
        "label": "Spring Mass",
        "value": "0.4"
      },
      {
        "label": "Blur Falloff",
        "value": "1.5px text / 0.6px avatar"
      },
      {
        "label": "Transition Curve",
        "value": "Hover: 300ms ease-out. Tooltip entrance: 200ms cubic-bezier(0.16, 1, 0.3, 1)"
      }
    ]
  },
  "accessibility": {
    "aria": "Quotes are inline spans in reading order. Each avatar's alt text is the author name. Role and company appear only in the mouse hover tooltip. The show-more button exposes its expanded state.",
    "reducedMotion": "With prefers-reduced-motion, hovering does not blur the other testimonials, so every quote stays at full contrast.",
    "keyboard": [
      {
        "key": "Tab",
        "description": "Focus expandable toggle button."
      },
      {
        "key": "Enter / Space",
        "description": "Toggle additional testimonials visibility."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Hero section social proof on modern SaaS and developer tool landing pages",
      "Community feedback sections highlighting user testimonials in high-density editorial format",
      "Product showcase pages where visual polish and delight are primary goals"
    ],
    "bestPractices": [
      "Keep quotes punchy and varied in length (15 to 30 words) for natural visual rhythm",
      "Use high-quality square avatar photos centered on faces",
      "Include a mix of industry leaders, developers, and community champions"
    ]
  },
  "props": [
    {
      "name": "initialItems",
      "type": "Testimonial[]",
      "defaultValue": "DEVCLUB_INITIAL_TESTIMONIALS",
      "description": "Array of primary testimonials displayed in the default view."
    },
    {
      "name": "additionalItems",
      "type": "Testimonial[]",
      "defaultValue": "DEVCLUB_ADDITIONAL_TESTIMONIALS",
      "description": "Array of additional testimonials revealed when expanding the list."
    },
    {
      "name": "className",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Optional CSS classes for outer container positioning."
    },
    {
      "name": "cardClassName",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Optional CSS classes for inner content wrapper."
    }
  ],
  "usage": "import { FocusTestimonials } from \"@/components/ui/focus-testimonials\";\n\nconst testimonials = [\n  {\n    id: 1,\n    author: \"Ada Lovelace\",\n    role: \"Engineer\",\n    company: \"Analytical Engines\",\n    avatar: \"/avatars/ada.jpg\",\n    quote: \"The components dropped straight into our app.\",\n  },\n];\n\nexport function FocusTestimonialsDemo() {\n  return <FocusTestimonials initialItems={testimonials} additionalItems={[]} />;\n}",
  "files": [
    {
      "name": "focus-testimonials.tsx",
      "path": "registry/ui/focus-testimonials.tsx",
      "type": "registry:ui",
      "target": "@ui/focus-testimonials.tsx",
      "code": "\"use client\";\n\nimport React, { useState, useRef, useCallback, useId, memo } from \"react\";\nimport {\n  motion,\n  AnimatePresence,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Testimonial {\n  id: number;\n  author: string;\n  role: string;\n  company: string;\n  avatar: string;\n  quote: string;\n}\n\nexport const DEVCLUB_INITIAL_TESTIMONIALS: Testimonial[] = [\n  {\n    id: 0,\n    author: \"Yash Sharma\",\n    role: \"Founding Engineer\",\n    company: \"DevClub NST\",\n    avatar:\n      \"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"DevClub UI saved my sleep schedule and centered all my divs on the very first try. Genuinely 10/10.\",\n  },\n  {\n    id: 1,\n    author: \"Jensen Huang\",\n    role: \"GPU Overlord\",\n    company: \"NVIDIA\",\n    avatar:\n      \"https://images.unsplash.com/photo-1552374196-c4e7ffc6e126?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"The more DevClub components you copy, the more time you save. It's the law of modern frontend physics.\",\n  },\n  {\n    id: 2,\n    author: \"Priya Nair\",\n    role: \"Ex-Burnout Lead\",\n    company: \"0 to 1 Startup\",\n    avatar:\n      \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"I copied three buttons from DevClub NST and suddenly our seed investors thought we hired Apple's design lead.\",\n  },\n  {\n    id: 3,\n    author: \"Cristiano Ronaldo\",\n    role: \"Perfectionist\",\n    company: \"CR7 Brand\",\n    avatar:\n      \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"Siiiuuu! The micro-interactions in this library have better curve control than my best free kicks.\",\n  },\n];\n\nexport const DEVCLUB_ADDITIONAL_TESTIMONIALS: Testimonial[] = [\n  {\n    id: 4,\n    author: \"Kabir Sen\",\n    role: \"Figma Whisperer\",\n    company: \"Studio Craft\",\n    avatar:\n      \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"Our product designer actually cried happy tears when my PR matched their Figma spring transitions on day one.\",\n  },\n  {\n    id: 5,\n    author: \"Sarah Chen\",\n    role: \"Junior Developer\",\n    company: \"HyperScale\",\n    avatar:\n      \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"I still don't fully understand GSAP timelines, but DevClub UI makes everyone think I have 8 years of WebGL mastery.\",\n  },\n  {\n    id: 6,\n    author: \"Vikram Das\",\n    role: \"Fullstack Vibe Engineer\",\n    company: \"NST Hackers\",\n    avatar:\n      \"https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"If DevClub NST goes offline, 90% of college startup landing pages will instantly collapse into plain HTML tables.\",\n  },\n  {\n    id: 7,\n    author: \"Elena Rostova\",\n    role: \"Head of Motion\",\n    company: \"Neon Dynamics\",\n    avatar:\n      \"https://images.unsplash.com/photo-1517841905240-472988babdf9?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"The spring dampening curves in these components are so silky they should honestly require a safety permit.\",\n  },\n  {\n    id: 8,\n    author: \"Aisha Patel\",\n    role: \"Product Manager\",\n    company: \"FlowState\",\n    avatar:\n      \"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"My CEO asked why our staging site looks like Linear and Apple had a baby. I just smiled and whispered 'DevClub'.\",\n  },\n  {\n    id: 9,\n    author: \"Devansh Rao\",\n    role: \"Hackathon Speedrunner\",\n    company: \"Speedrunners Inc\",\n    avatar:\n      \"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"Shipped 4 MVP submissions in 36 hours. Judges thought we didn't sleep for a week. We were just chilling with DevClub UI.\",\n  },\n  {\n    id: 10,\n    author: \"Sneha Reddy\",\n    role: \"VP of Centering Divs\",\n    company: \"CloudSync\",\n    avatar:\n      \"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"I used to spend half my sprint debugging Safari blur artifacts. With DevClub UI, everything just works out of the box.\",\n  },\n  {\n    id: 11,\n    author: \"Antony Raphy\",\n    role: \"Chief Vibe Officer\",\n    company: \"Indie Hacker\",\n    avatar:\n      \"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"Replaced 4,000 lines of spaghetti animation boilerplate with a single DevClub primitive. Finally touched grass.\",\n  },\n];\n\nconst TestimonialSpanItem = memo(function TestimonialSpanItem({\n  item,\n  isHovered,\n  hasHover,\n  onHover,\n}: {\n  item: Testimonial;\n  isHovered: boolean;\n  hasHover: boolean;\n  onHover: (id: number) => void;\n}) {\n  const stateClass = !hasHover\n    ? \"opacity-80 blur-0 text-zinc-600 dark:text-zinc-400\"\n    : isHovered\n      ? \"opacity-100 blur-0 text-zinc-950 dark:text-zinc-50\"\n      : \"opacity-25 blur-[1.5px] text-zinc-400 dark:text-zinc-600\";\n\n  const avatarClass = !hasHover\n    ? \"opacity-90 grayscale-[15%] scale-100\"\n    : isHovered\n      ? \"opacity-100 grayscale-0 scale-105 shadow-sm\"\n      : \"opacity-30 grayscale-[75%] blur-[0.6px] scale-[0.96]\";\n\n  return (\n    <span\n      onMouseEnter={() => onHover(item.id)}\n      className={cn(\n        \"inline cursor-pointer select-none transition-all duration-300 ease-out will-change-[opacity,filter,color] motion-reduce:transition-none motion-reduce:blur-none\",\n        stateClass,\n      )}\n    >\n      <span\n        className={cn(\n          \"inline-flex items-center justify-center align-middle mr-2 sm:mr-2.5 w-7 h-7 sm:w-8 sm:h-8 md:w-9 md:h-9 rounded-full overflow-hidden shrink-0 aspect-square select-none [clip-path:circle(50%_at_50%_50%)] transition-all duration-300 ease-out will-change-[transform,filter,opacity] motion-reduce:transition-none motion-reduce:blur-none motion-reduce:scale-100\",\n          avatarClass,\n        )}\n      >\n        <img\n          src={item.avatar}\n          alt={item.author}\n          width={44}\n          height={44}\n          loading=\"eager\"\n          className=\"block w-full h-full rounded-full object-cover select-none pointer-events-none [clip-path:circle(50%_at_50%_50%)]\"\n        />\n      </span>\n      {item.quote}\n      <span className=\"relative inline-block w-0 h-0 align-baseline\" />{\" \"}\n    </span>\n  );\n});\n\nexport interface FocusTestimonialsProps {\n  initialItems?: Testimonial[];\n  additionalItems?: Testimonial[];\n  className?: string;\n  cardClassName?: string;\n}\n\nexport function FocusTestimonials({\n  initialItems = DEVCLUB_INITIAL_TESTIMONIALS,\n  additionalItems = DEVCLUB_ADDITIONAL_TESTIMONIALS,\n  className,\n  cardClassName,\n}: FocusTestimonialsProps) {\n  const [hoveredId, setHoveredId] = useState<number | null>(null);\n  const [showMore, setShowMore] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const quotesId = useId();\n  const reduceMotion = useReducedMotion();\n\n  const rawMouseX = useMotionValue(0);\n  const rawMouseY = useMotionValue(0);\n\n  const springConfig = { damping: 28, stiffness: 340, mass: 0.4 };\n  const smoothX = useSpring(rawMouseX, springConfig);\n  const smoothY = useSpring(rawMouseY, springConfig);\n\n  const allItems = React.useMemo(\n    () => [...initialItems, ...additionalItems],\n    [initialItems, additionalItems],\n  );\n\n  const activeItem =\n    hoveredId !== null ? allItems.find((t) => t.id === hoveredId) : null;\n\n  const handleMouseMove = useCallback(\n    (e: React.MouseEvent<HTMLDivElement>) => {\n      if (!containerRef.current) return;\n      const rect = containerRef.current.getBoundingClientRect();\n      rawMouseX.set(e.clientX - rect.left);\n      rawMouseY.set(e.clientY - rect.top);\n    },\n    [rawMouseX, rawMouseY],\n  );\n\n  const handleMouseLeave = useCallback(() => {\n    setHoveredId(null);\n  }, []);\n\n  const handleHover = useCallback((id: number) => {\n    setHoveredId(id);\n  }, []);\n\n  const handleToggleShowMore = () => {\n    setShowMore((prev) => {\n      const nextState = !prev;\n      if (\n        !nextState &&\n        hoveredId !== null &&\n        hoveredId >= initialItems.length\n      ) {\n        setHoveredId(null);\n      }\n      return nextState;\n    });\n  };\n\n  const hasHover = hoveredId !== null;\n\n  return (\n    <div\n      className={cn(\n        \"relative flex w-full flex-col items-center justify-center overflow-hidden bg-transparent select-none\",\n        className,\n      )}\n    >\n      <div\n        ref={containerRef}\n        onMouseMove={handleMouseMove}\n        onMouseLeave={handleMouseLeave}\n        className={cn(\n          \"relative z-10 flex w-full max-w-5xl flex-col p-4 sm:p-6 md:p-8 transition-colors duration-200\",\n          cardClassName,\n        )}\n      >\n        <AnimatePresence>\n          {activeItem && (\n            <motion.div\n              key=\"author-tooltip\"\n              initial={{ opacity: 0, scale: 0.9, y: 8 }}\n              animate={{ opacity: 1, scale: 1, y: 0 }}\n              exit={{\n                opacity: 0,\n                scale: 0.9,\n                y: 6,\n                transition: reduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.15, ease: \"easeOut\" },\n              }}\n              transition={\n                reduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.2, ease: [0.16, 1, 0.3, 1] }\n              }\n              style={{\n                x: reduceMotion ? rawMouseX : smoothX,\n                y: reduceMotion ? rawMouseY : smoothY,\n                translateX: 18,\n                translateY: -56,\n              }}\n              className=\"pointer-events-none absolute left-0 top-0 z-50 flex items-center rounded-full border border-white/20 bg-zinc-950/90 px-3 py-1.5 text-white shadow-2xl backdrop-blur-xl will-change-[transform,opacity]\"\n            >\n              <AnimatePresence mode=\"wait\" initial={false}>\n                <motion.div\n                  key={activeItem.id}\n                  initial={{\n                    opacity: 0,\n                    filter: reduceMotion ? \"blur(0px)\" : \"blur(3px)\",\n                  }}\n                  animate={{ opacity: 1, filter: \"blur(0px)\" }}\n                  exit={{\n                    opacity: 0,\n                    filter: reduceMotion ? \"blur(0px)\" : \"blur(3px)\",\n                  }}\n                  transition={\n                    reduceMotion\n                      ? { duration: 0 }\n                      : { duration: 0.14, ease: \"easeOut\" }\n                  }\n                  className=\"flex items-center gap-2.5\"\n                >\n                  <div className=\"h-7 w-7 rounded-full overflow-hidden shrink-0 aspect-square [clip-path:circle(50%_at_50%_50%)]\">\n                    <img\n                      src={activeItem.avatar}\n                      alt={activeItem.author}\n                      width={28}\n                      height={28}\n                      className=\"block h-full w-full object-cover rounded-full border-0 outline-none shadow-none ring-0 [clip-path:circle(50%_at_50%_50%)] select-none pointer-events-none\"\n                    />\n                  </div>\n                  <div className=\"flex flex-col leading-tight select-none\">\n                    <span className=\"text-xs sm:text-sm font-semibold tracking-tight text-white whitespace-nowrap\">\n                      {activeItem.author}\n                    </span>\n                    <span className=\"text-[10px] sm:text-xs font-normal text-zinc-300 whitespace-nowrap\">\n                      {activeItem.role} ·{\" \"}\n                      <span className=\"font-medium text-white\">\n                        {activeItem.company}\n                      </span>\n                    </span>\n                  </div>\n                </motion.div>\n              </AnimatePresence>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        <div\n          id={quotesId}\n          onMouseLeave={() => setHoveredId(null)}\n          className=\"relative flex-1 text-xl sm:text-2xl md:text-3xl lg:text-[32px] font-normal leading-[160%] tracking-tight text-zinc-900 dark:text-zinc-100\"\n        >\n          {initialItems.map((item) => (\n            <TestimonialSpanItem\n              key={item.id}\n              item={item}\n              isHovered={hoveredId === item.id}\n              hasHover={hasHover}\n              onHover={handleHover}\n            />\n          ))}\n\n          <AnimatePresence>\n            {showMore && (\n              <motion.span\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={\n                  reduceMotion\n                    ? { duration: 0 }\n                    : { duration: 0.35, ease: \"easeOut\" }\n                }\n                className=\"inline\"\n              >\n                {\" \"}\n                {additionalItems.map((item) => (\n                  <TestimonialSpanItem\n                    key={item.id}\n                    item={item}\n                    isHovered={hoveredId === item.id}\n                    hasHover={hasHover}\n                    onHover={handleHover}\n                  />\n                ))}\n              </motion.span>\n            )}\n          </AnimatePresence>\n        </div>\n\n        <div className=\"mt-8 sm:mt-10 flex justify-center\">\n          <button\n            type=\"button\"\n            aria-expanded={showMore}\n            aria-controls={quotesId}\n            onClick={handleToggleShowMore}\n            className=\"group inline-flex min-h-10 items-center gap-1.5 rounded-md px-3 text-xs sm:text-sm tracking-tight text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors active:scale-[0.98] cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-white/40 motion-reduce:active:scale-100\"\n          >\n            <span>\n              {showMore\n                ? \"Show less\"\n                : `Read all testimonials (${allItems.length})`}\n            </span>\n\n            <motion.span\n              animate={{ rotate: showMore ? 180 : 0 }}\n              transition={\n                reduceMotion\n                  ? { duration: 0 }\n                  : { type: \"spring\", stiffness: 360, damping: 22 }\n              }\n              className=\"inline-flex\"\n            >\n              <ChevronDown className=\"h-3.5 w-3.5 text-current\" />\n            </motion.span>\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default FocusTestimonials;\n",
      "content": "\"use client\";\n\nimport React, { useState, useRef, useCallback, useId, memo } from \"react\";\nimport {\n  motion,\n  AnimatePresence,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Testimonial {\n  id: number;\n  author: string;\n  role: string;\n  company: string;\n  avatar: string;\n  quote: string;\n}\n\nexport const DEVCLUB_INITIAL_TESTIMONIALS: Testimonial[] = [\n  {\n    id: 0,\n    author: \"Yash Sharma\",\n    role: \"Founding Engineer\",\n    company: \"DevClub NST\",\n    avatar:\n      \"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"DevClub UI saved my sleep schedule and centered all my divs on the very first try. Genuinely 10/10.\",\n  },\n  {\n    id: 1,\n    author: \"Jensen Huang\",\n    role: \"GPU Overlord\",\n    company: \"NVIDIA\",\n    avatar:\n      \"https://images.unsplash.com/photo-1552374196-c4e7ffc6e126?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"The more DevClub components you copy, the more time you save. It's the law of modern frontend physics.\",\n  },\n  {\n    id: 2,\n    author: \"Priya Nair\",\n    role: \"Ex-Burnout Lead\",\n    company: \"0 to 1 Startup\",\n    avatar:\n      \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"I copied three buttons from DevClub NST and suddenly our seed investors thought we hired Apple's design lead.\",\n  },\n  {\n    id: 3,\n    author: \"Cristiano Ronaldo\",\n    role: \"Perfectionist\",\n    company: \"CR7 Brand\",\n    avatar:\n      \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"Siiiuuu! The micro-interactions in this library have better curve control than my best free kicks.\",\n  },\n];\n\nexport const DEVCLUB_ADDITIONAL_TESTIMONIALS: Testimonial[] = [\n  {\n    id: 4,\n    author: \"Kabir Sen\",\n    role: \"Figma Whisperer\",\n    company: \"Studio Craft\",\n    avatar:\n      \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"Our product designer actually cried happy tears when my PR matched their Figma spring transitions on day one.\",\n  },\n  {\n    id: 5,\n    author: \"Sarah Chen\",\n    role: \"Junior Developer\",\n    company: \"HyperScale\",\n    avatar:\n      \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"I still don't fully understand GSAP timelines, but DevClub UI makes everyone think I have 8 years of WebGL mastery.\",\n  },\n  {\n    id: 6,\n    author: \"Vikram Das\",\n    role: \"Fullstack Vibe Engineer\",\n    company: \"NST Hackers\",\n    avatar:\n      \"https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"If DevClub NST goes offline, 90% of college startup landing pages will instantly collapse into plain HTML tables.\",\n  },\n  {\n    id: 7,\n    author: \"Elena Rostova\",\n    role: \"Head of Motion\",\n    company: \"Neon Dynamics\",\n    avatar:\n      \"https://images.unsplash.com/photo-1517841905240-472988babdf9?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"The spring dampening curves in these components are so silky they should honestly require a safety permit.\",\n  },\n  {\n    id: 8,\n    author: \"Aisha Patel\",\n    role: \"Product Manager\",\n    company: \"FlowState\",\n    avatar:\n      \"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"My CEO asked why our staging site looks like Linear and Apple had a baby. I just smiled and whispered 'DevClub'.\",\n  },\n  {\n    id: 9,\n    author: \"Devansh Rao\",\n    role: \"Hackathon Speedrunner\",\n    company: \"Speedrunners Inc\",\n    avatar:\n      \"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"Shipped 4 MVP submissions in 36 hours. Judges thought we didn't sleep for a week. We were just chilling with DevClub UI.\",\n  },\n  {\n    id: 10,\n    author: \"Sneha Reddy\",\n    role: \"VP of Centering Divs\",\n    company: \"CloudSync\",\n    avatar:\n      \"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"I used to spend half my sprint debugging Safari blur artifacts. With DevClub UI, everything just works out of the box.\",\n  },\n  {\n    id: 11,\n    author: \"Antony Raphy\",\n    role: \"Chief Vibe Officer\",\n    company: \"Indie Hacker\",\n    avatar:\n      \"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=120&h=120&auto=format&fit=crop&crop=faces&q=80\",\n    quote:\n      \"Replaced 4,000 lines of spaghetti animation boilerplate with a single DevClub primitive. Finally touched grass.\",\n  },\n];\n\nconst TestimonialSpanItem = memo(function TestimonialSpanItem({\n  item,\n  isHovered,\n  hasHover,\n  onHover,\n}: {\n  item: Testimonial;\n  isHovered: boolean;\n  hasHover: boolean;\n  onHover: (id: number) => void;\n}) {\n  const stateClass = !hasHover\n    ? \"opacity-80 blur-0 text-zinc-600 dark:text-zinc-400\"\n    : isHovered\n      ? \"opacity-100 blur-0 text-zinc-950 dark:text-zinc-50\"\n      : \"opacity-25 blur-[1.5px] text-zinc-400 dark:text-zinc-600\";\n\n  const avatarClass = !hasHover\n    ? \"opacity-90 grayscale-[15%] scale-100\"\n    : isHovered\n      ? \"opacity-100 grayscale-0 scale-105 shadow-sm\"\n      : \"opacity-30 grayscale-[75%] blur-[0.6px] scale-[0.96]\";\n\n  return (\n    <span\n      onMouseEnter={() => onHover(item.id)}\n      className={cn(\n        \"inline cursor-pointer select-none transition-all duration-300 ease-out will-change-[opacity,filter,color] motion-reduce:transition-none motion-reduce:blur-none\",\n        stateClass,\n      )}\n    >\n      <span\n        className={cn(\n          \"inline-flex items-center justify-center align-middle mr-2 sm:mr-2.5 w-7 h-7 sm:w-8 sm:h-8 md:w-9 md:h-9 rounded-full overflow-hidden shrink-0 aspect-square select-none [clip-path:circle(50%_at_50%_50%)] transition-all duration-300 ease-out will-change-[transform,filter,opacity] motion-reduce:transition-none motion-reduce:blur-none motion-reduce:scale-100\",\n          avatarClass,\n        )}\n      >\n        <img\n          src={item.avatar}\n          alt={item.author}\n          width={44}\n          height={44}\n          loading=\"eager\"\n          className=\"block w-full h-full rounded-full object-cover select-none pointer-events-none [clip-path:circle(50%_at_50%_50%)]\"\n        />\n      </span>\n      {item.quote}\n      <span className=\"relative inline-block w-0 h-0 align-baseline\" />{\" \"}\n    </span>\n  );\n});\n\nexport interface FocusTestimonialsProps {\n  initialItems?: Testimonial[];\n  additionalItems?: Testimonial[];\n  className?: string;\n  cardClassName?: string;\n}\n\nexport function FocusTestimonials({\n  initialItems = DEVCLUB_INITIAL_TESTIMONIALS,\n  additionalItems = DEVCLUB_ADDITIONAL_TESTIMONIALS,\n  className,\n  cardClassName,\n}: FocusTestimonialsProps) {\n  const [hoveredId, setHoveredId] = useState<number | null>(null);\n  const [showMore, setShowMore] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const quotesId = useId();\n  const reduceMotion = useReducedMotion();\n\n  const rawMouseX = useMotionValue(0);\n  const rawMouseY = useMotionValue(0);\n\n  const springConfig = { damping: 28, stiffness: 340, mass: 0.4 };\n  const smoothX = useSpring(rawMouseX, springConfig);\n  const smoothY = useSpring(rawMouseY, springConfig);\n\n  const allItems = React.useMemo(\n    () => [...initialItems, ...additionalItems],\n    [initialItems, additionalItems],\n  );\n\n  const activeItem =\n    hoveredId !== null ? allItems.find((t) => t.id === hoveredId) : null;\n\n  const handleMouseMove = useCallback(\n    (e: React.MouseEvent<HTMLDivElement>) => {\n      if (!containerRef.current) return;\n      const rect = containerRef.current.getBoundingClientRect();\n      rawMouseX.set(e.clientX - rect.left);\n      rawMouseY.set(e.clientY - rect.top);\n    },\n    [rawMouseX, rawMouseY],\n  );\n\n  const handleMouseLeave = useCallback(() => {\n    setHoveredId(null);\n  }, []);\n\n  const handleHover = useCallback((id: number) => {\n    setHoveredId(id);\n  }, []);\n\n  const handleToggleShowMore = () => {\n    setShowMore((prev) => {\n      const nextState = !prev;\n      if (\n        !nextState &&\n        hoveredId !== null &&\n        hoveredId >= initialItems.length\n      ) {\n        setHoveredId(null);\n      }\n      return nextState;\n    });\n  };\n\n  const hasHover = hoveredId !== null;\n\n  return (\n    <div\n      className={cn(\n        \"relative flex w-full flex-col items-center justify-center overflow-hidden bg-transparent select-none\",\n        className,\n      )}\n    >\n      <div\n        ref={containerRef}\n        onMouseMove={handleMouseMove}\n        onMouseLeave={handleMouseLeave}\n        className={cn(\n          \"relative z-10 flex w-full max-w-5xl flex-col p-4 sm:p-6 md:p-8 transition-colors duration-200\",\n          cardClassName,\n        )}\n      >\n        <AnimatePresence>\n          {activeItem && (\n            <motion.div\n              key=\"author-tooltip\"\n              initial={{ opacity: 0, scale: 0.9, y: 8 }}\n              animate={{ opacity: 1, scale: 1, y: 0 }}\n              exit={{\n                opacity: 0,\n                scale: 0.9,\n                y: 6,\n                transition: reduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.15, ease: \"easeOut\" },\n              }}\n              transition={\n                reduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.2, ease: [0.16, 1, 0.3, 1] }\n              }\n              style={{\n                x: reduceMotion ? rawMouseX : smoothX,\n                y: reduceMotion ? rawMouseY : smoothY,\n                translateX: 18,\n                translateY: -56,\n              }}\n              className=\"pointer-events-none absolute left-0 top-0 z-50 flex items-center rounded-full border border-white/20 bg-zinc-950/90 px-3 py-1.5 text-white shadow-2xl backdrop-blur-xl will-change-[transform,opacity]\"\n            >\n              <AnimatePresence mode=\"wait\" initial={false}>\n                <motion.div\n                  key={activeItem.id}\n                  initial={{\n                    opacity: 0,\n                    filter: reduceMotion ? \"blur(0px)\" : \"blur(3px)\",\n                  }}\n                  animate={{ opacity: 1, filter: \"blur(0px)\" }}\n                  exit={{\n                    opacity: 0,\n                    filter: reduceMotion ? \"blur(0px)\" : \"blur(3px)\",\n                  }}\n                  transition={\n                    reduceMotion\n                      ? { duration: 0 }\n                      : { duration: 0.14, ease: \"easeOut\" }\n                  }\n                  className=\"flex items-center gap-2.5\"\n                >\n                  <div className=\"h-7 w-7 rounded-full overflow-hidden shrink-0 aspect-square [clip-path:circle(50%_at_50%_50%)]\">\n                    <img\n                      src={activeItem.avatar}\n                      alt={activeItem.author}\n                      width={28}\n                      height={28}\n                      className=\"block h-full w-full object-cover rounded-full border-0 outline-none shadow-none ring-0 [clip-path:circle(50%_at_50%_50%)] select-none pointer-events-none\"\n                    />\n                  </div>\n                  <div className=\"flex flex-col leading-tight select-none\">\n                    <span className=\"text-xs sm:text-sm font-semibold tracking-tight text-white whitespace-nowrap\">\n                      {activeItem.author}\n                    </span>\n                    <span className=\"text-[10px] sm:text-xs font-normal text-zinc-300 whitespace-nowrap\">\n                      {activeItem.role} ·{\" \"}\n                      <span className=\"font-medium text-white\">\n                        {activeItem.company}\n                      </span>\n                    </span>\n                  </div>\n                </motion.div>\n              </AnimatePresence>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        <div\n          id={quotesId}\n          onMouseLeave={() => setHoveredId(null)}\n          className=\"relative flex-1 text-xl sm:text-2xl md:text-3xl lg:text-[32px] font-normal leading-[160%] tracking-tight text-zinc-900 dark:text-zinc-100\"\n        >\n          {initialItems.map((item) => (\n            <TestimonialSpanItem\n              key={item.id}\n              item={item}\n              isHovered={hoveredId === item.id}\n              hasHover={hasHover}\n              onHover={handleHover}\n            />\n          ))}\n\n          <AnimatePresence>\n            {showMore && (\n              <motion.span\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={\n                  reduceMotion\n                    ? { duration: 0 }\n                    : { duration: 0.35, ease: \"easeOut\" }\n                }\n                className=\"inline\"\n              >\n                {\" \"}\n                {additionalItems.map((item) => (\n                  <TestimonialSpanItem\n                    key={item.id}\n                    item={item}\n                    isHovered={hoveredId === item.id}\n                    hasHover={hasHover}\n                    onHover={handleHover}\n                  />\n                ))}\n              </motion.span>\n            )}\n          </AnimatePresence>\n        </div>\n\n        <div className=\"mt-8 sm:mt-10 flex justify-center\">\n          <button\n            type=\"button\"\n            aria-expanded={showMore}\n            aria-controls={quotesId}\n            onClick={handleToggleShowMore}\n            className=\"group inline-flex min-h-10 items-center gap-1.5 rounded-md px-3 text-xs sm:text-sm tracking-tight text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors active:scale-[0.98] cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-white/40 motion-reduce:active:scale-100\"\n          >\n            <span>\n              {showMore\n                ? \"Show less\"\n                : `Read all testimonials (${allItems.length})`}\n            </span>\n\n            <motion.span\n              animate={{ rotate: showMore ? 180 : 0 }}\n              transition={\n                reduceMotion\n                  ? { duration: 0 }\n                  : { type: \"spring\", stiffness: 360, damping: 22 }\n              }\n              className=\"inline-flex\"\n            >\n              <ChevronDown className=\"h-3.5 w-3.5 text-current\" />\n            </motion.span>\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default FocusTestimonials;\n"
    }
  ]
}
