{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sync-pair",
  "type": "registry:component",
  "title": "Sync Pair",
  "description": "A laptop and a phone toss a document back and forth along a soft arc; when both edit at once, the two versions meet mid-air and merge into one with a check.",
  "categories": [
    "illustration",
    "Connections"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/sync-pair.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  motion,\n  type MotionValue,\n  useAnimationFrame,\n  useInView,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type SyncPairHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<SyncPairHue, string> = {\n  sky: \"[--hue:var(--color-sky-500)]\",\n  emerald: \"[--hue:var(--color-emerald-500)]\",\n  amber: \"[--hue:var(--color-amber-500)]\",\n  violet: \"[--hue:var(--color-violet-500)]\",\n  rose: \"[--hue:var(--color-rose-500)]\",\n  orange: \"[--hue:var(--color-orange-500)]\",\n  indigo: \"[--hue:var(--color-indigo-500)]\",\n  teal: \"[--hue:var(--color-teal-500)]\",\n};\n\nexport interface SyncPairProps {\n  title?: string;\n  description?: string;\n  laptopLabel?: string;\n  phoneLabel?: string;\n  hue?: SyncPairHue;\n  className?: string;\n}\n\ntype Pt = [number, number];\ntype Slot = \"a\" | \"b\" | \"ca\" | \"cb\";\ntype Phase = \"idle\" | \"toPhone\" | \"toLaptop\" | \"conflict\" | \"merging\" | \"merged\";\n\nconst W = 400;\nconst H = 146;\nconst FROM: Pt = [108, 58];\nconst TO: Pt = [320, 50];\nconst OX = -12;\nconst CTRL: Pt = [202, -8];\nconst APEX = 0.5;\nconst DOC = 1.35;\nconst LOOP = 11;\nconst RESET = 10.3;\nconst SETTLED = 9.4;\n\nconst at = {\n  typeA: 0.1,\n  sendA: 0.7,\n  landA: 1.6,\n  typeB: 2.5,\n  sendB: 3.1,\n  landB: 4,\n  typeC: 4.9,\n  rise: 5.5,\n  meet: 6.3,\n  merged: 6.8,\n  split: 7.5,\n  land: 8.4,\n};\n\nconst slots: Slot[] = [\"a\", \"b\", \"ca\", \"cb\"];\nconst fromLaptop: Record<Slot, boolean> = { a: true, b: false, ca: true, cb: false };\n\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\nconst seg = (t: number, a: number, b: number) => clamp01((t - a) / (b - a));\nconst ease = (v: number) => (v < 0.5 ? 4 * v * v * v : 1 - (-2 * v + 2) ** 3 / 2);\n\nfunction arc(t: number): Pt {\n  const u = 1 - t;\n  return [u * u * FROM[0] + 2 * u * t * CTRL[0] + t * t * TO[0], u * u * FROM[1] + 2 * u * t * CTRL[1] + t * t * TO[1]];\n}\n\ntype Flight = { p: number; dx: number; show: number };\n\nfunction edgeFade(p: number, from: number, to: number) {\n  const n = (p - from) / (to - from);\n  return clamp01(Math.min(n, 1 - n) * 8);\n}\n\nfunction flightA(t: number): Flight {\n  if (t >= at.sendA && t < at.landA) {\n    const p = ease(seg(t, at.sendA, at.landA));\n    return { p, dx: 0, show: edgeFade(p, 0, 1) };\n  }\n  if (t >= at.rise && t < at.merged) {\n    const p = APEX * ease(seg(t, at.rise, at.meet));\n    const dx = -22 * (1 - ease(seg(t, at.meet, at.merged)));\n    return { p, dx, show: Math.min(clamp01(p * 8 / APEX), 1 - seg(t, at.merged - 0.2, at.merged)) };\n  }\n  if (t >= at.split && t < at.land) {\n    const p = APEX * (1 - ease(seg(t, at.split, at.land)));\n    return { p, dx: 0, show: clamp01((p / APEX) * 8) };\n  }\n  return { p: 0, dx: 0, show: 0 };\n}\n\nfunction flightB(t: number): Flight {\n  if (t >= at.sendB && t < at.landB) {\n    const p = 1 - ease(seg(t, at.sendB, at.landB));\n    return { p, dx: 0, show: edgeFade(p, 0, 1) };\n  }\n  if (t >= at.rise && t < at.merged) {\n    const p = 1 - APEX * ease(seg(t, at.rise, at.meet));\n    const dx = 22 * (1 - ease(seg(t, at.meet, at.merged)));\n    return { p, dx, show: Math.min(clamp01(((1 - p) * 8) / APEX), 1 - seg(t, at.merged - 0.2, at.merged)) };\n  }\n  if (t >= at.split && t < at.land) {\n    const p = APEX + APEX * ease(seg(t, at.split, at.land));\n    return { p, dx: 0, show: clamp01(((1 - p) / APEX) * 8) };\n  }\n  return { p: 1, dx: 0, show: 0 };\n}\n\nfunction snapshot(t: number) {\n  const reset = t >= RESET;\n  const laptop: Record<Slot, \"off\" | \"typing\" | \"on\"> = {\n    a: reset ? \"off\" : t >= at.typeA ? \"typing\" : \"off\",\n    b: reset ? \"off\" : t >= at.landB ? \"on\" : \"off\",\n    ca: reset ? \"off\" : t >= at.typeC ? \"typing\" : \"off\",\n    cb: reset ? \"off\" : t >= at.land ? \"on\" : \"off\",\n  };\n  const phone: Record<Slot, \"off\" | \"typing\" | \"on\"> = {\n    a: reset ? \"off\" : t >= at.landA ? \"on\" : \"off\",\n    b: reset ? \"off\" : t >= at.typeB ? \"typing\" : \"off\",\n    ca: reset ? \"off\" : t >= at.land ? \"on\" : \"off\",\n    cb: reset ? \"off\" : t >= at.typeC ? \"typing\" : \"off\",\n  };\n  let phase: Phase = \"idle\";\n  if (!reset) {\n    if (t >= at.sendA && t < at.landA) phase = \"toPhone\";\n    else if (t >= at.sendB && t < at.landB) phase = \"toLaptop\";\n    else if (t >= at.typeC && t < at.meet) phase = \"conflict\";\n    else if (t >= at.meet && t < at.land) phase = \"merging\";\n    else if (t >= at.land) phase = \"merged\";\n  }\n  const landedLaptop = t >= at.land && !reset ? 2 : t >= at.landB && !reset ? 1 : 0;\n  const landedPhone = t >= at.land && !reset ? 2 : t >= at.landA && !reset ? 1 : 0;\n  return { laptop, phone, phase, landedLaptop, landedPhone, mergedDoc: t >= at.merged && t < at.split && !reset, mergedCopies: t >= at.split };\n}\n\nconst status: Record<Phase, { label: string; dot: string }> = {\n  idle: { label: \"Up to date\", dot: \"bg-success\" },\n  toPhone: { label: \"Syncing to phone\", dot: \"bg-info\" },\n  toLaptop: { label: \"Syncing to laptop\", dot: \"bg-info\" },\n  conflict: { label: \"2 versions found\", dot: \"bg-warning\" },\n  merging: { label: \"Merging changes\", dot: \"bg-info\" },\n  merged: { label: \"Merged · both edits kept\", dot: \"bg-success\" },\n};\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction Doc({ stripes }: { stripes: (\"hue\" | \"amber\")[] }) {\n  return (\n    <g transform={`scale(${DOC})`}>\n      <path className=\"fill-card stroke-foreground/20\" d=\"M-13 -16 H6 L13 -9 V16 H-13 Z\" strokeLinejoin=\"round\" strokeWidth={1.2} />\n      <path className=\"fill-foreground/10\" d=\"M6 -16 V-9 H13 Z\" />\n      <rect className=\"fill-foreground/35\" height={3} rx={1.5} width={12} x={-9} y={-11} />\n      <rect className=\"fill-foreground/15\" height={2.5} rx={1.25} width={17} x={-9} y={-4} />\n      {stripes.map((s, i) => (\n        <rect\n          className={s === \"hue\" ? \"fill-(--hue)\" : \"fill-amber-500\"}\n          height={2.5}\n          key={s}\n          rx={1.25}\n          width={i === 0 ? 16 : 12}\n          x={-9}\n          y={2 + i * 5}\n        />\n      ))}\n    </g>\n  );\n}\n\nfunction Flier({ clock, flight, stripes }: { clock: MotionValue<number>; flight: (t: number) => Flight; stripes: (\"hue\" | \"amber\")[] }) {\n  const x = useTransform(clock, (t) => {\n    const f = flight(t);\n    return arc(f.p)[0] + f.dx;\n  });\n  const y = useTransform(clock, (t) => arc(flight(t).p)[1]);\n  const opacity = useTransform(clock, (t) => flight(t).show);\n  const scale = useTransform(clock, (t) => 0.78 + 0.22 * Math.sin(Math.PI * flight(t).p));\n  const rotate = useTransform(clock, (t) => {\n    const f = flight(t);\n    return Math.cos(Math.PI * f.p) * -6;\n  });\n  return (\n    <motion.g style={{ opacity, rotate, scale, x, y }}>\n      <Doc stripes={stripes} />\n    </motion.g>\n  );\n}\n\nfunction Lines({\n  x,\n  ys,\n  widths,\n  height,\n  states,\n}: {\n  x: number;\n  ys: number[];\n  widths: number[];\n  height: number;\n  states: Record<Slot, \"off\" | \"typing\" | \"on\">;\n}) {\n  return (\n    <>\n      {slots.map((slot, i) => {\n        const state = states[slot];\n        return (\n          <rect\n            className={cn(\n              fromLaptop[slot] ? \"fill-(--hue)\" : \"fill-amber-500\",\n              \"origin-left [transform-box:fill-box] transition-[scale,opacity] ease-out\",\n              state === \"off\" ? \"scale-x-0 opacity-0 duration-300\" : \"scale-x-100 opacity-100\",\n              state === \"typing\" ? \"duration-[600ms]\" : state === \"on\" ? \"duration-300\" : \"\",\n            )}\n            height={height}\n            key={slot}\n            rx={height / 2}\n            width={widths[i]}\n            x={x}\n            y={ys[i]}\n          />\n        );\n      })}\n    </>\n  );\n}\n\nexport function SyncPair({\n  title = \"Always in sync\",\n  description = \"Edits hop between your devices in seconds. If two land at once, we keep both.\",\n  laptopLabel = \"Laptop\",\n  phoneLabel = \"Phone\",\n  hue = \"sky\",\n  className,\n}: SyncPairProps) {\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const live = useRef(false);\n  live.current = inView && !reduce;\n\n  const clock = useMotionValue(0);\n  useAnimationFrame((_, delta) => {\n    if (!live.current) return;\n    clock.set((clock.get() + Math.min(delta, 64) / 1000) % LOOP);\n  });\n  useEffect(() => {\n    if (reduce) clock.set(SETTLED);\n  }, [reduce, clock]);\n\n  const [snap, setSnap] = useState(() => snapshot(0));\n  const key = useRef(\"\");\n  useMotionValueEvent(clock, \"change\", (t) => {\n    const next = snapshot(t);\n    const k = JSON.stringify(next);\n    if (k === key.current) return;\n    key.current = k;\n    setSnap(next);\n  });\n\n  const apex = arc(APEX);\n  const s = status[snap.phase];\n  const copies: (\"hue\" | \"amber\")[] = [\"hue\", \"amber\"];\n\n  return (\n    <div\n      ref={root}\n      className={cn(\n        \"relative w-full max-w-[440px] overflow-hidden rounded-2xl border bg-card p-4 shadow-xs/5 sm:p-6\",\n        \"[--bezel:var(--color-neutral-800)] [--deck:var(--color-neutral-300)] dark:[--bezel:var(--color-neutral-700)] dark:[--deck:var(--color-neutral-600)]\",\n        hues[hue],\n        className,\n      )}\n    >\n      <div\n        aria-hidden\n        className=\"dot-grid pointer-events-none absolute inset-0 opacity-50 [mask-image:radial-gradient(closest-side,black,transparent)]\"\n      />\n\n      <div className=\"relative mx-auto mb-3 flex w-fit items-center gap-1.5 rounded-full border bg-popover py-1 pr-2.5 pl-2 text-xs shadow-xs/5\">\n        <span className={cn(\"size-2 rounded-full transition-colors duration-300\", s.dot)} />\n        <span className=\"font-medium text-foreground\">\n          {s.label}\n        </span>\n      </div>\n\n      <div\n        aria-label={`A document syncing between ${laptopLabel} and ${phoneLabel}`}\n        className=\"relative mx-auto aspect-[400/146] w-full max-w-[400px]\"\n        role=\"img\"\n      >\n        <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`${OX} 0 ${W} ${H}`}>\n          <path\n            className=\"fill-none stroke-foreground/15\"\n            d={`M${FROM[0]} ${FROM[1]} Q${CTRL[0]} ${CTRL[1]} ${TO[0]} ${TO[1]}`}\n            strokeDasharray=\"2 5\"\n            strokeLinecap=\"round\"\n            strokeWidth={1.5}\n          />\n\n          <g>\n            <rect className=\"fill-(--bezel)\" height={84} rx={9} width={132} x={42} y={48} />\n            <rect className=\"fill-background\" height={72} rx={4} width={120} x={48} y={54} />\n            <motion.rect\n              animate={snap.landedLaptop > 0 && !reduce ? { opacity: [0, 0.9, 0] } : { opacity: 0 }}\n              className=\"fill-none stroke-(--hue)\"\n              height={72}\n              key={`flash-l-${snap.landedLaptop}`}\n              rx={4}\n              strokeWidth={2}\n              transition={{ duration: 0.6, ease: \"easeOut\" }}\n              width={120}\n              x={48}\n              y={54}\n            />\n            <rect className=\"fill-foreground/40\" height={5} rx={2.5} width={40} x={58} y={63} />\n            <rect className=\"fill-foreground/12\" height={4} rx={2} width={92} x={58} y={74} />\n            <rect className=\"fill-foreground/12\" height={4} rx={2} width={74} x={58} y={82} />\n            <Lines height={4} states={snap.laptop} widths={[86, 62, 94, 70]} x={58} ys={[92, 100, 108, 116]} />\n            <path className=\"fill-(--deck)\" d=\"M28 132 H188 V135 Q188 140 182 140 H34 Q28 140 28 135 Z\" />\n            <rect className=\"fill-foreground/15\" height={3} rx={1.5} width={26} x={95} y={132} />\n          </g>\n\n          <g transform=\"translate(24 0)\">\n            <rect className=\"fill-(--bezel)\" height={96} rx={12} width={52} x={270} y={44} />\n            <rect className=\"fill-background\" height={90} rx={9} width={46} x={273} y={47} />\n            <motion.rect\n              animate={snap.landedPhone > 0 && !reduce ? { opacity: [0, 0.9, 0] } : { opacity: 0 }}\n              className=\"fill-none stroke-amber-500\"\n              height={90}\n              key={`flash-p-${snap.landedPhone}`}\n              rx={9}\n              strokeWidth={2}\n              transition={{ duration: 0.6, ease: \"easeOut\" }}\n              width={46}\n              x={273}\n              y={47}\n            />\n            <rect className=\"fill-(--bezel)\" height={4} rx={2} width={14} x={289} y={51} />\n            <rect className=\"fill-foreground/40\" height={4} rx={2} width={22} x={279} y={63} />\n            <rect className=\"fill-foreground/12\" height={3.5} rx={1.75} width={34} x={279} y={72} />\n            <rect className=\"fill-foreground/12\" height={3.5} rx={1.75} width={27} x={279} y={79} />\n            <Lines height={3.5} states={snap.phone} widths={[32, 24, 34, 26]} x={279} ys={[88, 95, 102, 109]} />\n            <rect className=\"fill-foreground/20\" height={2.5} rx={1.25} width={18} x={287} y={130} />\n          </g>\n\n          <Flier clock={clock} flight={flightA} stripes={snap.mergedCopies ? copies : [\"hue\"]} />\n          <Flier clock={clock} flight={flightB} stripes={snap.mergedCopies ? copies : [\"amber\"]} />\n\n          <motion.g\n            animate={snap.mergedDoc ? { opacity: 1, scale: 1 } : { opacity: 0, scale: reduce ? 1 : 0.95 }}\n            initial={false}\n            style={{ originX: `${apex[0]}px`, originY: `${apex[1]}px`, transformBox: \"view-box\" }}\n            transition={{ duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n          >\n            <g transform={`translate(${apex[0]} ${apex[1]})`}>\n              <Doc stripes={copies} />\n            </g>\n            <motion.g\n              animate={snap.mergedDoc ? { opacity: 1, scale: 1 } : { opacity: 0, scale: reduce ? 1 : 0.6 }}\n              initial={false}\n              style={{ originX: `${apex[0] + 13 * DOC}px`, originY: `${apex[1] + 15 * DOC}px`, transformBox: \"view-box\" }}\n              transition={reduce ? { duration: 0.15 } : { type: \"spring\", stiffness: 520, damping: 22, delay: snap.mergedDoc ? 0.12 : 0 }}\n            >\n              <circle className=\"fill-emerald-500 stroke-card\" cx={apex[0] + 13 * DOC} cy={apex[1] + 15 * DOC} r={8} strokeWidth={2} />\n              <path\n                className=\"fill-none stroke-white\"\n                d={`M${apex[0] + 13 * DOC - 3.6} ${apex[1] + 15 * DOC} l2.6 2.6 l4.4 -4.8`}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth={1.8}\n              />\n            </motion.g>\n          </motion.g>\n\n        </svg>\n      </div>\n\n      <div aria-hidden className=\"relative mx-auto h-4 w-full max-w-[400px] text-muted-foreground text-xs\">\n        {[\n          { label: laptopLabel, x: 108, dot: \"bg-(--hue)\" },\n          { label: phoneLabel, x: 320, dot: \"bg-amber-500\" },\n        ].map((d) => (\n          <span className=\"absolute top-0 flex -translate-x-1/2 items-center gap-1.5 whitespace-nowrap\" key={d.x} style={{ left: `${((d.x - OX) / W) * 100}%` }}>\n            <span className={cn(\"size-1.5 rounded-full\", d.dot)} />\n            {d.label}\n          </span>\n        ))}\n      </div>\n\n      <div className=\"relative mt-3 text-center\">\n        <p className=\"text-balance font-medium text-foreground text-sm\">{title}</p>\n        <p className=\"mx-auto mt-1 max-w-[36ch] text-pretty text-muted-foreground text-xs\">{description}</p>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}