{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"logo-cloud","type":"registry:block","title":"Logo Cloud","description":"Trusted-by logo strip with an infinite marquee variant behind edge fade masks that pauses on hover or focus, and a grid variant whose logos fade in with a stagger and lift on hover; logos start muted and grayscale and take their own colors on hover, accept any SVG or node, and fall back to a static row under reduced motion.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/logo-cloud.tsx","type":"registry:component","target":"@components/motion/logo-cloud.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/blocks/logo-cloud\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { Marquee } from \"@/components/motion/marquee\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface LogoCloudItem {\n  id: string;\n  /** Accessible name of the company or product. */\n  name: string;\n  /** SVG or any node. Use `currentColor` so it follows the muted/hover tint. */\n  logo: ReactNode;\n  href?: string;\n}\n\nexport interface LogoCloudProps {\n  logos: LogoCloudItem[];\n  /** \"marquee\" scrolls one infinite row; \"grid\" lays logos out and fades them in. Default \"marquee\". */\n  variant?: \"marquee\" | \"grid\";\n  eyebrow?: ReactNode;\n  heading?: ReactNode;\n  description?: ReactNode;\n  /** Seconds for one marquee loop. Default 30. */\n  speed?: number;\n  /** Marquee direction. Default \"left\". */\n  direction?: \"left\" | \"right\";\n  /** Pause the marquee on hover or keyboard focus. Default true. */\n  pauseOnHover?: boolean;\n  /** Logos start muted (grayscale, dimmed) and take their own colors on hover. Default true. */\n  colorize?: boolean;\n  /** Grid columns at the widest container size. Default 4. */\n  columns?: 3 | 4 | 5 | 6;\n  className?: string;\n  /** Classes for each logo tile (sizing, padding). */\n  itemClassName?: string;\n}\n\n// Static class maps: Tailwind only ships classes it can see written out.\nconst GRID_COLUMNS: Record<3 | 4 | 5 | 6, string> = {\n  3: \"grid-cols-2 @md:grid-cols-3\",\n  4: \"grid-cols-2 @md:grid-cols-4\",\n  5: \"grid-cols-2 @md:grid-cols-3 @2xl:grid-cols-5\",\n  6: \"grid-cols-3 @md:grid-cols-4 @2xl:grid-cols-6\",\n};\n\nconst STAGGER = 0.05;\n\nfunction LogoMark({\n  item,\n  colorize,\n  lift,\n  className,\n}: {\n  item: LogoCloudItem;\n  colorize: boolean;\n  lift: boolean;\n  className?: string;\n}) {\n  const classes = cn(\n    \"group/logo flex h-12 items-center justify-center px-3 text-muted-foreground outline-none transition-[color,filter,opacity,translate,border-color,box-shadow] duration-300 [&_svg]:h-7 [&_svg]:w-auto [&_svg]:max-w-full\",\n    colorize &&\n      \"opacity-70 grayscale hover:text-foreground hover:opacity-100 hover:grayscale-0 focus-visible:text-foreground focus-visible:opacity-100 focus-visible:grayscale-0\",\n    // Tailwind's hover variant only applies on hover-capable pointers, so\n    // touch devices never get a stuck lift.\n    lift && \"hover:-translate-y-1 motion-reduce:hover:translate-y-0\",\n    item.href && \"rounded-xl focus-visible:ring-2 focus-visible:ring-ring/50\",\n    className,\n  );\n  const content = (\n    <>\n      <span aria-hidden className=\"contents\">\n        {item.logo}\n      </span>\n      <span className=\"sr-only\">{item.name}</span>\n    </>\n  );\n  return item.href ? (\n    <a href={item.href} className={classes}>\n      {content}\n    </a>\n  ) : (\n    <div className={classes}>{content}</div>\n  );\n}\n\n/**\n * \"Trusted by\" logo strip. The marquee variant reuses `Marquee` for one\n * infinite row behind edge fade masks, pausing on hover or focus. The grid\n * variant fades logos in with a stagger when it scrolls into view; each\n * logo lifts on hover. Logos start muted and take their own colors on hover.\n * Reduced motion shows the marquee's logos as a static wrapped row.\n */\nexport function LogoCloud({\n  logos,\n  variant = \"marquee\",\n  eyebrow,\n  heading,\n  description,\n  speed = 30,\n  direction = \"left\",\n  pauseOnHover = true,\n  colorize = true,\n  columns = 4,\n  className,\n  itemClassName,\n}: LogoCloudProps) {\n  const reduce = useReducedMotion();\n\n  const header =\n    eyebrow || heading || description ? (\n      <div className=\"mx-auto mb-6 flex max-w-xl flex-col items-center gap-2 text-center\">\n        {eyebrow ? (\n          <p className=\"text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground\">\n            {eyebrow}\n          </p>\n        ) : null}\n        {heading ? (\n          <h2 className=\"text-balance font-display text-xl font-semibold leading-tight tracking-tight text-foreground @3xl:text-2xl\">\n            {heading}\n          </h2>\n        ) : null}\n        {description ? (\n          <p className=\"text-pretty text-sm leading-6 text-muted-foreground\">\n            {description}\n          </p>\n        ) : null}\n      </div>\n    ) : null;\n\n  let body: ReactNode;\n  if (variant === \"marquee\" && !reduce) {\n    body = (\n      <Marquee\n        direction={direction}\n        speed={speed}\n        pauseOnHover={pauseOnHover}\n        gap=\"1.5rem\"\n        className={cn(\n          \"py-2\",\n          pauseOnHover && \"[&:focus-within>div]:[animation-play-state:paused]\",\n        )}\n      >\n        {logos.map((item) => (\n          <LogoMark\n            key={item.id}\n            item={item}\n            colorize={colorize}\n            lift={false}\n            className={itemClassName}\n          />\n        ))}\n      </Marquee>\n    );\n  } else if (variant === \"marquee\") {\n    // Reduced motion: no endless movement, just a centered wrapped row.\n    body = (\n      <ul className=\"flex flex-wrap items-center justify-center gap-x-6 gap-y-3\">\n        {logos.map((item) => (\n          <li key={item.id}>\n            <LogoMark item={item} colorize={colorize} lift={false} className={itemClassName} />\n          </li>\n        ))}\n      </ul>\n    );\n  } else {\n    body = (\n      <motion.ul\n        initial=\"hidden\"\n        whileInView=\"show\"\n        viewport={{ once: true, amount: 0.3 }}\n        variants={{ show: { transition: { staggerChildren: STAGGER } } }}\n        className={cn(\"grid gap-3\", GRID_COLUMNS[columns])}\n      >\n        {logos.map((item) => (\n          <motion.li\n            key={item.id}\n            variants={{\n              hidden: reduce ? { opacity: 0 } : { opacity: 0, y: 10, scale: 0.96 },\n              show: {\n                opacity: 1,\n                y: 0,\n                scale: 1,\n                transition: { duration: 0.5, ease: EASE_OUT },\n              },\n            }}\n          >\n            <LogoMark\n              item={item}\n              colorize={colorize}\n              lift\n              className={cn(\n                \"h-20 rounded-2xl border border-border bg-card shadow-xs hover:border-border-strong hover:shadow-sm\",\n                itemClassName,\n              )}\n            />\n          </motion.li>\n        ))}\n      </motion.ul>\n    );\n  }\n\n  return (\n    <section className={cn(\"@container w-full\", className)}>\n      {header}\n      {body}\n    </section>\n  );\n}\n"},{"path":"components/motion/marquee.tsx","type":"registry:component","target":"@components/motion/marquee.tsx","content":"import { Children, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MarqueeProps {\n  children: ReactNode;\n  direction?: \"left\" | \"right\" | \"up\" | \"down\";\n  speed?: number;\n  pauseOnHover?: boolean;\n  gap?: string;\n  className?: string;\n  fade?: boolean;\n}\n\nexport function Marquee({\n  children,\n  direction = \"left\",\n  speed = 30,\n  pauseOnHover = true,\n  gap = \"1rem\",\n  className,\n  fade = true,\n}: MarqueeProps) {\n  const vertical = direction === \"up\" || direction === \"down\";\n  const reverse = direction === \"right\" || direction === \"down\";\n  const items = Children.toArray(children);\n\n  return (\n    <div\n      className={cn(\n        \"group relative flex overflow-hidden\",\n        vertical ? \"flex-col\" : \"flex-row\",\n        fade && !vertical && \"[mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]\",\n        fade && vertical && \"[mask-image:linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)]\",\n        className,\n      )}\n      // gap on the wrapper too, so the seam between the two tracks matches the\n      // spacing between items and the loop stays even.\n      style={{ \"--gap\": gap, gap } as React.CSSProperties}\n    >\n      {[0, 1].map((dup) => (\n        <div\n          key={dup}\n          aria-hidden={dup === 1}\n          inert={dup === 1}\n          style={{\n            animationDuration: `${speed}s`,\n            animationDirection: reverse ? \"reverse\" : \"normal\",\n            gap,\n          }}\n          className={cn(\n            \"flex shrink-0 items-center\",\n            vertical ? \"flex-col animate-marquee-vertical\" : \"flex-row animate-marquee\",\n            pauseOnHover && \"group-hover:[animation-play-state:paused]\",\n          )}\n        >\n          {items.map((child, i) => (\n            // biome-ignore lint/suspicious/noArrayIndexKey: Marquee duplicates static child slots; item order is not mutated.\n            <div key={i} className=\"shrink-0\">\n              {child}\n            </div>\n          ))}\n        </div>\n      ))}\n    </div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}