{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-scatter-focus",
  "type": "registry:block",
  "title": "Team Scatter Focus",
  "description": "A scattered team collage whose cards each fly in from their own direction, then blur and shrink back from whichever one you hover. Falls back to a stacked grid below lg.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/team-scatter-focus.tsx",
      "type": "registry:component",
      "target": "components/tweenui/team-scatter-focus.tsx",
      "content": "'use client';\n\nimport { useRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n/** Blurring the rest is a pointer affordance — skip it on touch. */\nconst canHover = () =>\n  typeof window !== 'undefined' &&\n  window.matchMedia('(hover: hover) and (pointer: fine)').matches &&\n  !prefersReducedMotion();\n\nconst REVEAL_OFFSET = 80;\n\nconst DIRECTION_FROM: Record<string, { x?: number; y?: number }> = {\n  left: { x: -REVEAL_OFFSET },\n  right: { x: REVEAL_OFFSET },\n  up: { y: -REVEAL_OFFSET },\n  down: { y: REVEAL_OFFSET },\n};\n\n/**\n * Scatter slots, used in order for members that don't carry a `position` of\n * their own — so an arbitrary list still lands as a collage rather than a stack.\n */\nconst DEFAULT_POSITIONS = [\n  'z-2 left-[3%] top-[84px] xl:left-0',\n  'z-1 left-[20%] top-[50%] xl:left-[22%] xl:top-[31%]',\n  'z-1 left-[35%] top-[0%] xl:left-[43%]',\n  'z-1 left-[47%] bottom-[0%] xl:left-[56%]',\n  'z-1 right-[3%] top-[40%] -translate-y-1/2 xl:right-0 xl:top-[50%]',\n];\n\nconst DEFAULT_DIRECTIONS = ['left', 'down', 'up', 'down', 'right'];\n\nconst XMark = ({ className }: { className?: string }) => {\n  return (\n    <svg viewBox=\"0 0 24 24\" className={className} aria-hidden=\"true\">\n      <path\n        d=\"M18.9 2.3h3.4l-7.4 8.5 8.7 11.5h-6.8l-5.4-7-6.1 7H1.9l7.9-9.1L1.5 2.3h7l4.9 6.4 5.5-6.4Zm-1.2 17.9h1.9L6.4 4.2H4.4l13.3 16Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n};\n\nexport interface TeamScatterFocusMember {\n  /** Member name. */\n  name: string;\n  /** Role shown under the name. */\n  role: string;\n  /** Portrait URL. */\n  image: string;\n  /** Alt text for the portrait. */\n  imageAlt?: string;\n  /** Link to the member's page. */\n  href?: string;\n  /** Social profile opened from the corner button. */\n  social?: string;\n  /** Absolute placement classes for the desktop collage. Falls back by index. */\n  position?: string;\n  /** Where the card travels in from. Falls back by index. */\n  direction?: 'left' | 'right' | 'up' | 'down';\n}\n\nexport interface TeamScatterFocusProps extends Omit<ComponentPropsWithoutRef<'section'>, 'title'> {\n  /** Section heading. */\n  title?: ReactNode;\n  /** Supporting line under the heading. */\n  description?: string;\n  /** Members in the collage. Defaults to a five-person sample. */\n  members?: TeamScatterFocusMember[];\n}\n\nconst DEFAULT_TITLE = 'The people who make it happen';\n\nconst DEFAULT_DESCRIPTION =\n  'Each member brings deep expertise and a shared focus — building solutions that are not only beautiful but built to perform.';\n\nconst PORTRAIT = (id: string) => `https://images.unsplash.com/${id}?w=660&h=612&fit=crop`;\n\nconst DEFAULT_MEMBERS: TeamScatterFocusMember[] = [\n  {\n    name: 'John Smith',\n    role: 'CEO & Founder',\n    image: PORTRAIT('photo-1507003211169-0a1dd7228f2d'),\n    imageAlt: 'John Smith in the studio',\n    social: 'https://x.com',\n  },\n  {\n    name: 'John Lacker',\n    role: 'Creative Director',\n    image: PORTRAIT('photo-1519085360753-af0119f7cbe7'),\n    imageAlt: 'John Lacker reviewing artwork',\n    social: 'https://x.com',\n  },\n  {\n    name: 'William Finley',\n    role: 'Lead Designer',\n    image: PORTRAIT('photo-1500648767791-00dcc994a43e'),\n    imageAlt: 'William Finley at his desk',\n    social: 'https://x.com',\n  },\n  {\n    name: 'Micheal Jordan',\n    role: 'Account Director',\n    image: PORTRAIT('photo-1472099645785-5658abf4ff4e'),\n    imageAlt: 'Micheal Jordan on a client call',\n    social: 'https://x.com',\n  },\n  {\n    name: 'Jack Lavis',\n    role: 'Senior Developer',\n    image: PORTRAIT('photo-1521119989659-a83eee488004'),\n    imageAlt: 'Jack Lavis pairing on code',\n    social: 'https://x.com',\n  },\n];\n\nconst MemberCard = ({\n  member,\n  variant,\n  cardRef,\n}: {\n  member: TeamScatterFocusMember;\n  variant: 'collage' | 'stacked';\n  cardRef?: (node: HTMLDivElement | null) => void;\n}) => {\n  const href = member.href ?? '#';\n  const isCollage = variant === 'collage';\n\n  return (\n    <div\n      ref={cardRef}\n      data-team-card\n      className={cn(\n        'rounded-xl bg-white px-2 pt-2 pb-4 will-change-transform dark:bg-[#161b22]',\n        'motion-reduce:transform-none motion-reduce:blur-none motion-reduce:transition-none',\n        isCollage ? 'h-[332px] w-[264px] space-y-4' : 'w-full max-w-[330px] space-y-5'\n      )}\n    >\n      <figure\n        className={cn(\n          'relative overflow-hidden rounded-md',\n          isCollage ? 'h-[246px] w-[248px]' : 'aspect-[315/306] w-full'\n        )}\n      >\n        <a href={href}>\n          <img\n            src={member.image}\n            alt={member.imageAlt ?? member.name}\n            loading=\"lazy\"\n            className=\"size-full object-cover\"\n          />\n        </a>\n      </figure>\n\n      <div className={cn('space-y-1', isCollage ? 'px-2' : 'px-3')}>\n        <div className=\"flex items-center justify-between gap-2\">\n          <a href={href} className=\"min-w-0\">\n            <h3\n              className={cn(\n                'truncate font-medium text-[#12161f] dark:text-white',\n                isCollage ? 'text-base' : 'text-lg'\n              )}\n            >\n              {member.name}\n            </h3>\n          </a>\n          {member.social && (\n            <a\n              href={member.social}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              aria-label={`${member.name} on X`}\n              className={cn(\n                'grid shrink-0 place-items-center rounded-full bg-[#f1f3f4] text-[#13193a] transition-colors hover:bg-[#e6e9ea] dark:bg-white/5 dark:text-white dark:hover:bg-white/10',\n                isCollage ? 'size-8' : 'size-9'\n              )}\n            >\n              <XMark className={isCollage ? 'size-3.5' : 'size-4'} />\n            </a>\n          )}\n        </div>\n        <a href={href}>\n          <p\n            className={cn(\n              'text-[#18181b]/60 dark:text-white/60',\n              isCollage ? 'text-xs' : 'text-sm'\n            )}\n          >\n            {member.role}\n          </p>\n        </a>\n      </div>\n    </div>\n  );\n};\n\nconst TeamScatterFocus = ({\n  title = DEFAULT_TITLE,\n  description = DEFAULT_DESCRIPTION,\n  members = DEFAULT_MEMBERS,\n  className,\n  ...props\n}: TeamScatterFocusProps) => {\n  const rootRef = useRef<HTMLElement>(null);\n  const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const slotRefs = useRef<(HTMLDivElement | null)[]>([]);\n\n  useGSAP(\n    () => {\n      if (prefersReducedMotion()) return;\n\n      const slots = slotRefs.current.filter(Boolean) as HTMLDivElement[];\n      slots.forEach((slot, index) => {\n        const direction =\n          members[index]?.direction ?? DEFAULT_DIRECTIONS[index % DEFAULT_DIRECTIONS.length];\n        // `fromTo` with an explicit end state, and `opacity` rather than\n        // `autoAlpha`: nothing is ever left at `visibility: hidden`, so the\n        // cards stay in the accessibility tree while the reveal is pending.\n        gsap.fromTo(\n          slot,\n          { ...DIRECTION_FROM[direction], opacity: 0 },\n          {\n            x: 0,\n            y: 0,\n            opacity: 1,\n            duration: 0.9,\n            ease: 'power3.out',\n            delay: 0.1 * (index + 1),\n          }\n        );\n      });\n    },\n    { scope: rootRef, dependencies: [members] }\n  );\n\n  /** Hold the hovered card sharp and push the rest back. */\n  const handleEnter = (index: number) => {\n    if (!canHover()) return;\n\n    const slot = slotRefs.current[index];\n    if (slot) gsap.set(slot, { zIndex: 20 });\n\n    cardRefs.current.forEach((card, idx) => {\n      if (!card) return;\n      gsap.to(card, {\n        scale: idx === index ? 1 : 0.96,\n        filter: idx === index ? 'blur(0px)' : 'blur(3px)',\n        duration: 0.9,\n        ease: 'power2.out',\n        overwrite: 'auto',\n      });\n    });\n  };\n\n  const handleLeave = () => {\n    if (!canHover()) return;\n\n    slotRefs.current.forEach((slot) => {\n      if (slot) gsap.set(slot, { clearProps: 'zIndex' });\n    });\n\n    const cards = cardRefs.current.filter(Boolean) as HTMLDivElement[];\n    gsap.to(cards, {\n      scale: 1,\n      filter: 'blur(0px)',\n      duration: 0.55,\n      ease: 'power2.out',\n      overwrite: 'auto',\n    });\n  };\n\n  return (\n    <section\n      ref={rootRef}\n      data-team-scatter-focus\n      className={cn('w-full px-5 py-16 md:py-24', className)}\n      {...props}\n    >\n      <div className=\"mx-auto max-w-[1290px] space-y-10 md:space-y-16\">\n        <div className=\"space-y-3 text-center\">\n          <h2 className=\"text-3xl font-medium tracking-tight text-[#12161f] md:text-5xl dark:text-white\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-[600px] text-base text-[#18181b]/60 md:text-lg dark:text-white/60\">\n            {description}\n          </p>\n        </div>\n\n        {/* Collage — desktop only, cards placed by absolute slots. */}\n        {/* Heights track the card: the lowest slot is `top-[50%]` (`top-[31%]`\n            at xl), so the box needs roughly twice the card height below lg. */}\n        <div className=\"relative hidden h-[680px] overflow-hidden lg:block xl:h-[540px]\">\n          {members.map((member, index) => (\n            <div\n              key={member.name}\n              ref={(node) => {\n                slotRefs.current[index] = node;\n              }}\n              className={cn(\n                'absolute',\n                member.position ?? DEFAULT_POSITIONS[index % DEFAULT_POSITIONS.length]\n              )}\n              onMouseEnter={() => handleEnter(index)}\n              onMouseLeave={handleLeave}\n            >\n              <MemberCard\n                member={member}\n                variant=\"collage\"\n                cardRef={(node) => {\n                  cardRefs.current[index] = node;\n                }}\n              />\n            </div>\n          ))}\n        </div>\n\n        {/* Below lg the collage would overlap, so the same cards stack. */}\n        <div className=\"grid grid-cols-12 justify-items-center gap-y-6 md:gap-6 lg:hidden\">\n          {members.map((member, index) => (\n            <div\n              key={member.name}\n              className={cn(\n                'col-span-12 md:col-span-6',\n                index === members.length - 1 && index % 2 === 0 && 'md:col-start-4'\n              )}\n            >\n              <MemberCard member={member} variant=\"stacked\" />\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default TeamScatterFocus;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/team-scatter-focus\nThe sample content loads images from images.unsplash.com. Replace them with your own before shipping."
}