{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "focus-testimonials",
  "type": "registry:ui",
  "title": "Focus Testimonials",
  "description": "Fluid paragraph testimonials featuring inline avatars, cursor-tracking spring physics author tooltip, and dynamic selective blur focus on hover.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/focus-testimonials.tsx",
      "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",
      "type": "registry:ui",
      "target": "@ui/focus-testimonials.tsx"
    }
  ]
}
