{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "integration-hub",
  "type": "registry:block",
  "title": "Integration Hub",
  "description": "A hub-and-spoke integrations diagram: the center pops in, paths draw out to six logos, then a dot travels each line. Uses MotionPathPlugin.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/integration-hub.tsx",
      "type": "registry:component",
      "target": "components/tweenui/integration-hub.tsx",
      "content": "'use client';\n\nimport { useRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { MotionPathPlugin } from 'gsap/MotionPathPlugin';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, MotionPathPlugin);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport interface IntegrationHubLogo {\n  src: string;\n  /** Optional mark shown under `.dark`. */\n  srcDark?: string;\n  alt: string;\n}\n\nexport interface IntegrationHubProps extends Omit<ComponentPropsWithoutRef<'section'>, 'title'> {\n  /** Section heading. */\n  title?: ReactNode;\n  /** Supporting line under the heading. */\n  description?: string;\n  /** Three logos on the left (top, middle, bottom). */\n  left?: IntegrationHubLogo[];\n  /** Three logos on the right (top, middle, bottom). */\n  right?: IntegrationHubLogo[];\n}\n\nconst DEFAULT_LEFT: IntegrationHubLogo[] = [\n  { src: 'https://cdn.simpleicons.org/telegram/26A5E4', alt: 'Telegram' },\n  { src: 'https://cdn.simpleicons.org/google/4285F4', alt: 'Google' },\n  {\n    src: 'https://cdn.simpleicons.org/notion/000000',\n    srcDark: 'https://cdn.simpleicons.org/notion/ffffff',\n    alt: 'Notion',\n  },\n];\n\nconst DEFAULT_RIGHT: IntegrationHubLogo[] = [\n  { src: 'https://cdn.simpleicons.org/reddit/FF4500', alt: 'Reddit' },\n  { src: 'https://cdn.simpleicons.org/stripe/635BFF', alt: 'Stripe' },\n  { src: 'https://cdn.simpleicons.org/zoom/2D8CFF', alt: 'Zoom' },\n];\n\nconst PATHS = [\n  {\n    transform: 'translate(449,240) scale(1.5) translate(-227,-147.703)',\n    d: 'M227 147.703H192.062C172.52 147.703 154.769 136.319 146.619 118.558L105.908 29.8482C97.7568 12.0876 80.006 0.703125 60.4644 0.703125H0',\n  },\n  {\n    transform: 'translate(500,250) scale(1.7) translate(-227,0)',\n    d: 'M227 0.703125H0',\n  },\n  {\n    transform: 'translate(450,262) scale(1.5) translate(-227,0)',\n    d: 'M227 0.703132H192.062C172.52 0.703132 154.769 12.0876 146.619 29.8482L105.908 118.558C97.7568 136.319 80.006 147.703 60.4644 147.703H0',\n  },\n  {\n    transform: 'translate(555,238) scale(1.5) translate(0,-147.703)',\n    d: 'M0 147.703H34.9383C54.4799 147.703 72.2306 136.319 80.3814 118.558L121.092 29.8482C129.243 12.0876 146.994 0.703125 166.536 0.703125H227',\n  },\n  {\n    transform: 'translate(500,250) scale(1.7) translate(0,0)',\n    d: 'M0 0.703125H227',\n  },\n  {\n    transform: 'translate(566,263) scale(1.5) translate(0,0)',\n    d: 'M0 0.703132H34.9383C54.4799 0.703132 72.2306 12.0876 80.3814 29.8482L121.092 118.558C129.243 136.319 146.994 147.703 166.536 147.703H227',\n  },\n] as const;\n\n/** Path ends in the 1000×500 viewBox. Keep the diagram at aspect-[2/1]. */\nconst NODE_SLOTS = [\n  { x: '10.9%', y: '3.9%', side: 'left' },\n  { x: '11.4%', y: '50.2%', side: 'left' },\n  { x: '11.0%', y: '96.7%', side: 'left' },\n  { x: '89.6%', y: '3.5%', side: 'right' },\n  { x: '88.6%', y: '50.2%', side: 'right' },\n  { x: '90.7%', y: '96.9%', side: 'right' },\n] as const;\n\nfunction HubMark() {\n  return (\n    <svg viewBox=\"0 0 24 24\" className=\"size-8 fill-white lg:size-10\" aria-hidden=\"true\">\n      <path d=\"M8 5H10V7H8V5Z\" />\n      <path d=\"M11 8H13V10H11V8Z\" />\n      <path d=\"M14 11H16V13H14V11Z\" />\n      <path d=\"M11 14H13V16H11V14Z\" />\n      <path d=\"M8 17H10V19H8V17Z\" />\n    </svg>\n  );\n}\n\nfunction LogoNode({ logo, slot }: { logo: IntegrationHubLogo; slot: (typeof NODE_SLOTS)[number] }) {\n  return (\n    <div\n      className={cn(\n        'absolute z-10 -translate-y-1/2',\n        slot.side === 'left' ? '-translate-x-full' : null\n      )}\n      style={{ left: slot.x, top: slot.y }}\n    >\n      <div\n        data-hub-node\n        className=\"relative grid size-14 scale-0 place-items-center rounded-3xl border border-[#045f64]/10 bg-white motion-reduce:scale-100 sm:size-16 md:size-[4.5rem] dark:border-white/10 dark:bg-[#12161F]\"\n      >\n        <div className=\"absolute top-1/2 left-1/2 size-10 -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-[#045f64]/10 sm:size-12 md:size-14 dark:border-white/10\" />\n        <figure className=\"size-9 overflow-hidden rounded-xl bg-white sm:size-11 md:size-12 dark:bg-transparent\">\n          {logo.srcDark ? (\n            <>\n              <img\n                src={logo.src}\n                alt={logo.alt}\n                className=\"size-full object-contain p-2 dark:hidden\"\n              />\n              <img\n                src={logo.srcDark}\n                alt=\"\"\n                aria-hidden=\"true\"\n                className=\"hidden size-full object-contain p-2 dark:block\"\n              />\n            </>\n          ) : (\n            <img src={logo.src} alt={logo.alt} className=\"size-full object-contain p-2\" />\n          )}\n        </figure>\n      </div>\n    </div>\n  );\n}\n\nexport default function IntegrationHub({\n  title = 'One hub for every integration',\n  description = 'Connect Notion, Zoom, Stripe, and the rest of your stack without leaving your product.',\n  left = DEFAULT_LEFT,\n  right = DEFAULT_RIGHT,\n  className,\n  ...props\n}: IntegrationHubProps) {\n  const rootRef = useRef<HTMLElement>(null);\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      if (!root) return;\n\n      const diagram = root.querySelector<HTMLElement>('[data-hub-diagram]');\n      const rings = gsap.utils.toArray<HTMLElement>('[data-hub-ring]', root);\n      const paths = gsap.utils.toArray<SVGPathElement>('[data-hub-path]', root);\n      const dots = gsap.utils.toArray<SVGCircleElement>('[data-hub-dot]', root);\n      const nodes = gsap.utils.toArray<HTMLElement>('[data-hub-node]', root);\n\n      if (!diagram || rings.length < 1 || paths.length < 1) return;\n\n      const canDraw = paths.every((path) => typeof path.getTotalLength === 'function');\n\n      if (prefersReducedMotion() || !canDraw) {\n        gsap.set([...rings, ...nodes], { scale: 1 });\n        gsap.set(paths, { opacity: 1, strokeDashoffset: 0, strokeDasharray: 'none' });\n        gsap.set(dots, { autoAlpha: 0 });\n        return;\n      }\n\n      gsap.set(rings, { scale: 0, transformOrigin: '50% 50%' });\n      gsap.set(nodes, { scale: 0, transformOrigin: '50% 50%' });\n      paths.forEach((path) => {\n        const len = path.getTotalLength();\n        gsap.set(path, { strokeDasharray: len, strokeDashoffset: len, opacity: 1 });\n      });\n      gsap.set(dots, { autoAlpha: 0 });\n\n      const tl = gsap.timeline();\n      tl.to(rings, { scale: 1, duration: 0.55, ease: 'back.out(1.15)' });\n      tl.to(\n        paths,\n        { strokeDashoffset: 0, duration: 0.85, stagger: 0.06, ease: 'power2.inOut' },\n        '-=0.1'\n      );\n      tl.to(nodes, { scale: 1, duration: 0.4, stagger: 0.07, ease: 'back.out(1.2)' }, '-=0.05');\n\n      tl.add(() => {\n        paths.forEach((path, index) => {\n          const dot = dots[index];\n          if (!dot) return;\n          const tween = gsap.to(dot, {\n            duration: 4,\n            repeat: -1,\n            ease: 'none',\n            paused: true,\n            motionPath: {\n              path,\n              align: path,\n              alignOrigin: [0.5, 0.5],\n              autoRotate: false,\n              start: 0,\n              end: 1,\n            },\n          });\n          gsap.set(dot, { autoAlpha: 1 });\n          tween.progress((0.45 + index * 0.08) % 1).play();\n        });\n      });\n    },\n    { scope: rootRef }\n  );\n\n  return (\n    <section\n      {...props}\n      ref={rootRef}\n      data-integration-hub\n      className={cn('w-full py-16 md:py-20', className)}\n    >\n      <div className=\"mx-auto w-full max-w-[804px] space-y-16 px-4\">\n        <div className=\"space-y-6 text-center\">\n          <h2 className=\"text-2xl leading-[1.15] font-medium tracking-tight text-[#12161F] md:text-3xl lg:text-4xl dark:text-white\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-[400px] text-sm leading-relaxed text-[#045f64]/70 dark:text-[#9fd4d6]/80\">\n            {description}\n          </p>\n        </div>\n\n        <div data-hub-diagram className=\"relative mx-auto aspect-[2/1] w-full max-w-full\">\n          <svg\n            className=\"absolute inset-0 h-full w-full\"\n            viewBox=\"0 0 1000 500\"\n            preserveAspectRatio=\"xMidYMid meet\"\n            aria-hidden=\"true\"\n          >\n            {PATHS.map((path) => (\n              <g key={path.transform} transform={path.transform}>\n                <path\n                  data-hub-path\n                  className=\"fill-none stroke-[#045f64]/15 stroke-1 opacity-0 motion-reduce:opacity-100 dark:stroke-white/15\"\n                  d={path.d}\n                />\n                <circle\n                  data-hub-dot\n                  className=\"fill-[#045f64] opacity-0 motion-reduce:hidden dark:fill-[#c6f56f]\"\n                  r={2}\n                />\n              </g>\n            ))}\n          </svg>\n\n          <div className=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2\">\n            <div\n              data-hub-ring\n              className=\"relative grid size-20 scale-0 place-items-center rounded-full border border-[#045f64]/10 bg-white/80 backdrop-blur-[20px] motion-reduce:scale-100 md:size-36 lg:size-40 dark:border-white/10 dark:bg-[#12161F]/80\"\n            >\n              <div className=\"absolute inset-[14%] rounded-full border border-[#045f64]/10 dark:border-white/10\" />\n              <figure className=\"relative z-10 grid size-16 place-items-center rounded-full bg-[linear-gradient(90deg,#045f64_0%,#c6f56f_100%)] md:size-20\">\n                <HubMark />\n              </figure>\n            </div>\n          </div>\n\n          {left.map((logo, index) => (\n            <LogoNode key={logo.alt} logo={logo} slot={NODE_SLOTS[index] ?? NODE_SLOTS[0]} />\n          ))}\n          {right.map((logo, index) => (\n            <LogoNode key={logo.alt} logo={logo} slot={NODE_SLOTS[index + 3] ?? NODE_SLOTS[3]} />\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/integration-hub\nGSAP plugins (MotionPathPlugin) are registered inside the component, so there is nothing to set up.\nThe sample content loads images from cdn.simpleicons.org. Replace them with your own before shipping."
}