{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "puzzle-snap",
  "title": "Puzzle Snap",
  "description": "Geometric pieces spin in from scattered positions and magnet-snap into a mark with a spark burst, then the mark slides aside for the wordmark.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/puzzle-snap/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  random,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type PuzzleSnapProps = {\n  /** Wordmark revealed beside the assembled mark. */\n  children?: React.ReactNode;\n  backgroundColor?: string;\n  /** Fill of the quarter circle and the small square. */\n  primaryColor?: string;\n  /** Fill of the dot and the wedge. */\n  secondaryColor?: string;\n  /** Spark burst color. */\n  sparkColor?: string;\n  /** Include the small square piece. */\n  square?: boolean;\n  /** Seed for the scattered start positions. */\n  seed?: number;\n  speed?: number;\n};\n\nexport const puzzleSnapDefaults = {\n  backgroundColor: \"#111111\",\n  primaryColor: \"#E5322D\",\n  secondaryColor: \"#FFFFFF\",\n  sparkColor: \"#FFFFFF\",\n  square: true,\n  seed: 3,\n  speed: 1,\n};\n\nconst STAGGER = 3;\nconst DRIFT = 10;\nconst SNAP = 3;\nconst SPARK = 8;\nconst SLIDE = 11;\nconst clamp = {\n  extrapolateLeft: \"clamp\" as const,\n  extrapolateRight: \"clamp\" as const,\n};\n\ntype Piece = {\n  key: string;\n  /** Pivot / contact point in mark units (0..2). */\n  px: number;\n  py: number;\n  render: (c: number) => React.ReactNode;\n};\n\nexport function PuzzleSnap(props: PuzzleSnapProps) {\n  const {\n    children,\n    backgroundColor,\n    primaryColor,\n    secondaryColor,\n    sparkColor,\n    square,\n    seed,\n    speed,\n  } = { ...puzzleSnapDefaults, ...props };\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const t = frame * Math.max(0, speed);\n  const s = Math.min(width, height);\n  const size = Math.round(s * 0.2);\n  const c = size / 2;\n  const gap = c * 0.08;\n\n  const pieces: Piece[] = [\n    {\n      key: \"quarter\",\n      px: 0.6,\n      py: 0.6,\n      render: (u) => (\n        <path\n          d={`M ${u - gap} ${u - gap} L 0 ${u - gap} A ${u - gap} ${u - gap} 0 0 1 ${u - gap} 0 Z`}\n          fill={primaryColor}\n        />\n      ),\n    },\n    {\n      key: \"dot\",\n      px: 1.5,\n      py: 0.5,\n      render: (u) => (\n        <circle cx={u * 1.5} cy={u * 0.5} r={u * 0.36} fill={secondaryColor} />\n      ),\n    },\n    {\n      key: \"wedge\",\n      px: 0.65,\n      py: 1.35,\n      render: (u) => (\n        <path\n          d={`M ${u - gap} ${u + gap} L ${u - gap} ${2 * u} L 0 ${2 * u} Z`}\n          fill={secondaryColor}\n        />\n      ),\n    },\n    ...(square\n      ? [\n          {\n            key: \"square\",\n            px: 1.5,\n            py: 1.5,\n            render: (u: number) => (\n              <rect\n                x={u * 1.18}\n                y={u * 1.18}\n                width={u * 0.64}\n                height={u * 0.64}\n                rx={u * 0.08}\n                fill={primaryColor}\n              />\n            ),\n          },\n        ]\n      : []),\n  ];\n  const lastContact = (pieces.length - 1) * STAGGER + DRIFT + SNAP;\n\n  const punch = spring({\n    frame: t - lastContact,\n    fps,\n    config: { damping: 9, stiffness: 260, mass: 0.5 },\n  });\n  const markScale = t < lastContact ? 1 : 1 + 0.08 * (1 - punch);\n  const slide = interpolate(\n    t,\n    [lastContact + 2, lastContact + 2 + SLIDE],\n    [0, 1],\n    {\n      ...clamp,\n      easing: Easing.inOut(Easing.cubic),\n    },\n  );\n  const reveal = interpolate(\n    t,\n    [lastContact + 4, lastContact + 4 + SLIDE],\n    [0, 1],\n    { ...clamp, easing: Easing.out(Easing.cubic) },\n  );\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: size * 0.24,\n          // Shift the row so the mark sits dead center until it slides over.\n          transform: `translateX(calc((50% - ${size / 2}px) * ${children ? 1 - slide : 0}))`,\n        }}\n      >\n        <svg\n          width={size}\n          height={size}\n          viewBox={`0 0 ${size} ${size}`}\n          style={{\n            flexShrink: 0,\n            overflow: \"visible\",\n            transform: `scale(${markScale})`,\n          }}\n        >\n          {pieces.map((piece, i) => {\n            const local = t - i * STAGGER;\n            const angle = random(`puzzle-snap-${seed}-${i}-a`) * Math.PI * 2;\n            const dist =\n              s * (0.42 + 0.22 * random(`puzzle-snap-${seed}-${i}-d`));\n            const spin =\n              (random(`puzzle-snap-${seed}-${i}-r`) > 0.5 ? 1 : -1) *\n              (120 + 160 * random(`puzzle-snap-${seed}-${i}-s`));\n            // Drift most of the way in, then get yanked onto the slot.\n            const remain =\n              local < DRIFT\n                ? interpolate(local, [0, DRIFT], [1, 0.2], {\n                    ...clamp,\n                    easing: Easing.out(Easing.cubic),\n                  })\n                : interpolate(local, [DRIFT, DRIFT + SNAP], [0.2, 0], {\n                    ...clamp,\n                    easing: Easing.in(Easing.quad),\n                  });\n            const ox = Math.cos(angle) * dist * remain;\n            const oy = Math.sin(angle) * dist * remain;\n            const pivotX = piece.px * c;\n            const pivotY = piece.py * c;\n            const opacity = interpolate(local, [0, 3], [0, 1], clamp);\n            const sparkT = (local - DRIFT - SNAP) / SPARK;\n            return (\n              <g key={piece.key}>\n                <g\n                  opacity={opacity}\n                  transform={`translate(${ox} ${oy}) rotate(${spin * remain} ${pivotX} ${pivotY})`}\n                >\n                  {piece.render(c)}\n                </g>\n                {sparkT >= 0 &&\n                  sparkT < 1 &&\n                  Array.from({ length: 6 }, (_, k) => {\n                    const a =\n                      (k / 6) * Math.PI * 2 +\n                      random(`puzzle-snap-${seed}-${i}-spark`) * Math.PI;\n                    const e = Easing.out(Easing.cubic)(sparkT);\n                    const r0 = c * (0.45 + 0.4 * e);\n                    const r1 = r0 + c * 0.28 * (1 - 0.5 * e);\n                    return (\n                      <line\n                        key={k}\n                        x1={pivotX + Math.cos(a) * r0}\n                        y1={pivotY + Math.sin(a) * r0}\n                        x2={pivotX + Math.cos(a) * r1}\n                        y2={pivotY + Math.sin(a) * r1}\n                        stroke={sparkColor}\n                        strokeWidth={Math.max(1, c * 0.05 * (1 - sparkT))}\n                        strokeLinecap=\"round\"\n                        opacity={1 - sparkT}\n                      />\n                    );\n                  })}\n              </g>\n            );\n          })}\n        </svg>\n        {children && (\n          <div\n            style={{\n              opacity: reveal,\n              clipPath: `inset(-30% ${(1 - reveal) * 100}% -30% -5%)`,\n              transform: `translateX(${(1 - reveal) * -size * 0.3}px)`,\n            }}\n          >\n            {children}\n          </div>\n        )}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/puzzle-snap.tsx"
    }
  ],
  "type": "registry:component"
}
