{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logo-assemble",
  "title": "Logo Assemble",
  "description": "A single ring of image cards revolves around a centred title, then spins inward and drains to the middle — giving birth to a simple logo that slides left as the brand name reveals to its right.",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "https://snapcn.dev/r/snap-cn-ui.json"
  ],
  "files": [
    {
      "path": "registry/snap-cn/logo-assemble/index.tsx",
      "content": "\"use client\";\n\nimport {\n  Easing,\n  getRemotionEnvironment,\n  Img,\n  interpolate,\n  staticFile,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport {\n  mixOklch,\n  type SnapCnTheme,\n  useSnapCnTheme,\n  withAlpha,\n} from \"@/lib/snap-cn-ui\";\n\n/**\n * A logo build: a single ring of image cards revolves around the centre, then\n * spins inward and vanishes into the middle — giving birth to a simple logo and\n * the brand name. One circle, no colour on the mark; the images do the motion.\n */\n\nexport interface LogoAssembleProps {\n  /** The logo mark (kept simple/monochrome — it sits on the dark backdrop). */\n  logoSrc?: string;\n  /** Brand name that appears to the right of the logo. */\n  brandName?: string;\n  /** Text held in the middle of the ring while the images revolve (\\n for lines). */\n  middleText?: string;\n  /** Image URLs for the ring, cycled around the circle. */\n  images?: string[];\n  /** How many cards ride the ring. */\n  count?: number;\n  /** Backdrop color. Overrides the design system's `background`. */\n  background?: string;\n  speed?: number;\n  className?: string;\n  /** Design-system token overrides. */\n  theme?: Partial<SnapCnTheme>;\n  /**\n   * Defaults to `\"dark\"`: the shipped `logoSrc` is a white mark, so the stage\n   * has to be dark for it to read. Pass `\"light\"` with a dark logo asset.\n   */\n  mode?: \"light\" | \"dark\";\n}\n\nconst FONT_FAMILY =\n  'Inter, var(--font-geist-sans), -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif';\n\n/** Sample images. Absolute, because a root-relative default becomes\n *  `staticFile()` in a render and 404s in the project this file was just\n *  copied into. Pass your own — root-relative or remote, both resolve. */\nconst DEFAULT_IMAGES = [\n  \"https://snapcn.dev/showcase-assets/438b9e6b50654a44d404fbf358c26e9f.webp\",\n  \"https://snapcn.dev/showcase-assets/5e5305b05bd405a0d89570725434099e.webp\",\n  \"https://snapcn.dev/showcase-assets/767d99bb371a54d0d36751e8cecae43c.jpg\",\n  \"https://snapcn.dev/showcase-assets/821d815affa6496c39cbdeeec7a84603.jpg\",\n  \"https://snapcn.dev/showcase-assets/937438c560ada1c83317f2c11b3454b0.jpg\",\n  \"https://snapcn.dev/showcase-assets/98f89cb9994f5c382ab964062c4039db.jpg\",\n  \"https://snapcn.dev/showcase-assets/b25b82db2892efff9be3204e860d30ee.jpg\",\n  \"https://snapcn.dev/showcase-assets/c9ebc6337aa2268ac4b357f9cb1ac547.jpg\",\n];\n\n// ─── Timeline (frames @ 30fps; ~3.6s) ──────────────────────────────────────────\n/** The ring spins inward from here (radius → 0, cards vanish). */\nexport const CONTRACT_START = 50;\nexport const CONTRACT_END = 66;\n/** The middle text fades out as the ring collapses. */\nexport const MIDDLE_OUT = 52;\n/** The logo is born at the centre, then slides left to make room for the name. */\nexport const LOGO_IN = 58;\nexport const LOGO_SHIFT_START = 74;\nexport const LOGO_SHIFT_END = 90;\n/** The name reveals in step with the slide — the logo moving and the name\n *  coming happen simultaneously, not one after the other. */\nexport const NAME_IN = LOGO_SHIFT_START;\n\n/** Base angular speed of the ring (radians / frame). */\nexport const ORBIT_SPEED = 0.1;\n/** Extra spin added as the ring drains inward — the dr, water-down-the-plughole. */\nexport const DRAIN_SPIN = Math.PI * 1.1;\nconst CARD_RATIO = 1.22; // portrait cards\n\nexport const EASE = Easing.bezier(0.2, 0.6, 0.35, 1);\n/** Accelerate into the centre — the collapse, not a drift. */\nexport const EASE_IN = Easing.bezier(0.5, 0, 0.75, 0.4);\nconst CLAMP = { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" } as const;\n\n/** 0 while orbiting, 1 when fully drained to the centre. */\nexport function contractProgress(frame: number): number {\n  return interpolate(frame, [CONTRACT_START, CONTRACT_END], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IN,\n  });\n}\n\n/**\n * The ring's rotation (radians). Steady while orbiting, then a burst of extra\n * spin as it drains inward.\n */\nexport function orbitAngle(frame: number): number {\n  return frame * ORBIT_SPEED + contractProgress(frame) * DRAIN_SPIN;\n}\n\n/** Radius multiplier: 1 while orbiting, → 0 as the ring drains in. */\nexport function ringScale(frame: number): number {\n  return 1 - contractProgress(frame);\n}\n\n/** Card scale: full size on the ring, shrinking as it pulls to the centre. */\nexport function cardScale(frame: number): number {\n  return 1 - 0.66 * contractProgress(frame);\n}\n\n/** Card opacity: solid on the ring, vanishing near the end of the drain. */\nexport function cardOpacity(frame: number): number {\n  return interpolate(frame, [CONTRACT_END - 8, CONTRACT_END], [1, 0], CLAMP);\n}\n\n/** Center text: fades in early, holds through the orbit, out as the ring drains. */\nexport function middleTextOpacity(frame: number): number {\n  return interpolate(\n    frame,\n    [6, 16, MIDDLE_OUT - 6, MIDDLE_OUT],\n    [0, 1, 1, 0],\n    CLAMP,\n  );\n}\n\nexport interface LogoPose {\n  opacity: number;\n  scale: number;\n}\n\n/** The logo is born at the centre — a small overshoot as the ring vanishes. */\nexport function logoPose(frame: number): LogoPose {\n  return {\n    opacity: interpolate(frame, [LOGO_IN, LOGO_IN + 8], [0, 1], CLAMP),\n    scale: interpolate(\n      frame,\n      [LOGO_IN, LOGO_IN + 10, LOGO_IN + 18],\n      [0.6, 1.05, 1],\n      { ...CLAMP, easing: EASE },\n    ),\n  };\n}\n\n/** Fraction the logo has slid from centre toward its left resting spot (0 → 1). */\nexport function logoShift(frame: number): number {\n  return interpolate(frame, [LOGO_SHIFT_START, LOGO_SHIFT_END], [0, 1], {\n    ...CLAMP,\n    easing: EASE,\n  });\n}\n\nexport interface NamePose {\n  opacity: number;\n  dx: number;\n}\n\n/** Brand name: fades and slides in from the right, after the logo has moved. */\nexport function namePose(frame: number): NamePose {\n  return {\n    opacity: interpolate(frame, [NAME_IN, NAME_IN + 10], [0, 1], CLAMP),\n    dx: interpolate(frame, [NAME_IN, NAME_IN + 12], [18, 0], {\n      ...CLAMP,\n      easing: EASE,\n    }),\n  };\n}\n\n/** Rewrite root-relative assets through staticFile only while rendering. */\nfunction resolveSrc(src: string): string {\n  const isLocal = src.startsWith(\"/\") && !src.startsWith(\"//\");\n  if (isLocal && getRemotionEnvironment().isRendering) {\n    return staticFile(src.replace(/^\\/+/, \"\"));\n  }\n  return src;\n}\n\nexport function LogoAssemble({\n  logoSrc = \"https://snapcn.dev/logo/snapcn-white.png\",\n  brandName = \"snapcn\",\n  middleText = \"Cinematic components\\nfor React\",\n  images = DEFAULT_IMAGES,\n  count = 10,\n  background,\n  speed = 1,\n  className,\n  theme,\n  mode,\n}: LogoAssembleProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width, height } = useVideoConfig();\n  const t = useSnapCnTheme(theme, mode ?? \"dark\");\n  const stage = background ?? t.background;\n  // A shadow is cast in the dark direction whatever the mode, so it is walked\n  // from the stage toward black — not toward `foreground`, which is near-white\n  // in dark mode and would light the card from below instead of grounding it.\n  const cardShadow = withAlpha(mixOklch(stage, \"#000\", 0.75), 0.4);\n  const cx = width / 2;\n  const cy = height / 2;\n  const short = Math.min(width, height);\n  const isRendering = getRemotionEnvironment().isRendering;\n\n  const R = short * 0.36;\n  // Smaller than the arc between cards, so the ring has clear gaps.\n  const cardW = Math.round(short * 0.15);\n  const cardH = cardW * CARD_RATIO;\n\n  const a = orbitAngle(frame);\n  const rs = ringScale(frame);\n  const cScale = cardScale(frame);\n  const cOpacity = cardOpacity(frame);\n  const middleOp = middleTextOpacity(frame);\n  const logo = logoPose(frame);\n  const shift = logoShift(frame);\n  const name = namePose(frame);\n  const n = Math.max(1, Math.floor(count));\n\n  // Logo slides from the centre to a left resting spot; the name sits to its right.\n  const logoSize = Math.round(short * 0.125);\n  const gap = Math.round(short * 0.035);\n  const restX = short * 0.13; // how far left the logo settles\n  const logoCX = cx - restX * shift;\n  const nameLeft = cx - restX + logoSize / 2 + gap;\n  const nameSize = Math.round(short * 0.055);\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        backgroundColor: stage,\n        fontFamily: FONT_FAMILY,\n        textRendering: \"geometricPrecision\",\n      }}\n    >\n      {/* The ring of images */}\n      {cOpacity > 0 &&\n        Array.from({ length: n }, (_, i) => {\n          const theta = (i / n) * Math.PI * 2 + a;\n          const x = cx + Math.cos(theta) * R * rs;\n          const y = cy + Math.sin(theta) * R * rs;\n          // Cards stay upright and just revolve around the centre — no tangent\n          // spin, so no image ever tips onto its side.\n          return (\n            <div\n              // biome-ignore lint/suspicious/noArrayIndexKey: the ring is a fixed positional set — the index IS the card's identity\n              key={`card-${i}`}\n              style={{\n                position: \"absolute\",\n                left: x - cardW / 2,\n                top: y - cardH / 2,\n                width: cardW,\n                height: cardH,\n                opacity: cOpacity,\n                transform: `scale(${cScale})`,\n                overflow: \"hidden\",\n                borderRadius: 4,\n                boxShadow: `0 14px 34px ${cardShadow}`,\n                ...(isRendering ? {} : { willChange: \"transform\" as const }),\n              }}\n            >\n              <Img\n                src={resolveSrc(images[i % images.length] ?? images[0])}\n                style={{\n                  position: \"absolute\",\n                  inset: 0,\n                  width: \"100%\",\n                  height: \"100%\",\n                  maxWidth: \"none\",\n                  objectFit: \"cover\",\n                }}\n              />\n            </div>\n          );\n        })}\n\n      {/* Center text, held in the ring while the images revolve */}\n      {middleOp > 0 && middleText && (\n        <div\n          style={{\n            position: \"absolute\",\n            left: cx - R,\n            top: cy - cardH,\n            width: R * 2,\n            height: cardH * 2,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            textAlign: \"center\",\n            opacity: middleOp,\n            color: t.foreground,\n            fontSize: Math.round(short * 0.04),\n            fontWeight: 600,\n            lineHeight: 1.2,\n            letterSpacing: \"-0.02em\",\n            whiteSpace: \"pre-line\",\n          }}\n        >\n          {middleText}\n        </div>\n      )}\n\n      {/* Simple logo, born at the centre, then sliding left */}\n      {logo.opacity > 0 && (\n        <Img\n          src={resolveSrc(logoSrc)}\n          style={{\n            position: \"absolute\",\n            left: logoCX - logoSize / 2,\n            top: cy - logoSize / 2,\n            width: logoSize,\n            height: logoSize,\n            maxWidth: \"none\",\n            objectFit: \"contain\",\n            opacity: logo.opacity,\n            transform: `scale(${logo.scale})`,\n            transformOrigin: \"center center\",\n          }}\n        />\n      )}\n\n      {/* Brand name, revealed to the right of the settled logo */}\n      {name.opacity > 0 && (\n        <div\n          style={{\n            position: \"absolute\",\n            left: nameLeft,\n            top: cy - nameSize,\n            height: nameSize * 2,\n            display: \"flex\",\n            alignItems: \"center\",\n            opacity: name.opacity,\n            transform: `translateX(${name.dx}px)`,\n            color: t.foreground,\n            fontSize: nameSize,\n            fontWeight: 600,\n            letterSpacing: \"-0.02em\",\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {brandName}\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/snap-cn/logo-assemble.tsx"
    }
  ],
  "type": "registry:component"
}