{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dock",
  "title": "Dock",
  "author": "AlgorizeTech",
  "description": "A macOS-style dock where icons magnify as the pointer approaches. Use it for app launchers and playful navigation bars.",
  "dependencies": [
    "gsap"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/DockBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport gsap from \"gsap\";\nimport React, { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { bezierEase, gsapTicker } from \"./gsap-motion\";\nimport { createSpringFollower, MOTION_EASE, LAYOUT_TRANSITION } from \"./motion-curves\";\n\nexport interface DockItem {\n  id: string;\n  label: string;\n  icon: React.ReactNode;\n  onClick?: () => void;\n  isActive?: boolean;\n  badge?: string | number;\n}\n\nexport interface DockProps extends React.HTMLAttributes<HTMLDivElement> {\n  items: DockItem[];\n  baseSize?: number;\n  magnification?: number;\n  distance?: number;\n  containerHeight?: number | string;\n  gap?: number;\n}\n\n/** Magnification spring — the same constants the Motion build uses. */\nconst SIZE_SPRING = { mass: 0.1, stiffness: 150, damping: 12 };\n\n/**\n * Tooltip in/out: 0.15s tweens with no ease given, which Motion resolves to\n * `easeOut` for every value (animateMotionValue's default).\n */\nconst TOOLTIP_EASE = bezierEase(MOTION_EASE.easeOut);\nconst DOT_EASE = bezierEase(LAYOUT_TRANSITION.ease);\n\n/** Pointer position shared by every icon; `Infinity` while the pointer is away. */\ntype PointerBus = {\n  x: number;\n  listeners: Set<(x: number) => void>;\n};\n\n/** Where the active dot last sat, so the next one can fly in from there. */\ntype DotMemory = { rect: DOMRect | null };\n\n/** A `useRef` box handed to the icons (typed locally so React 18 and 19 types agree). */\ntype Held<T> = { readonly current: T };\n\nexport const Dock = React.forwardRef<HTMLDivElement, DockProps>(\n  (\n    {\n      items,\n      baseSize = 48,\n      magnification = 80,\n      distance = 140,\n      containerHeight = 64,\n      gap = 12,\n      className,\n      style,\n      onMouseMove,\n      onMouseLeave,\n      ...props\n    },\n    ref,\n  ) => {\n    const bus = useRef<PointerBus>({ x: Infinity, listeners: new Set() });\n    const dot = useRef<DotMemory>({ rect: null });\n    const [screenTier, setScreenTier] = useState<\"desktop\" | \"tablet\" | \"mobile\">(\"desktop\");\n\n    // Automatically detect small screens to scale down physics on touch devices\n    useEffect(() => {\n      const handleResize = () => {\n        const w = window.innerWidth;\n        if (w < 480) {\n          setScreenTier(\"mobile\");\n        } else if (w < 640) {\n          setScreenTier(\"tablet\");\n        } else {\n          setScreenTier(\"desktop\");\n        }\n      };\n      handleResize();\n      window.addEventListener(\"resize\", handleResize);\n      return () => window.removeEventListener(\"resize\", handleResize);\n    }, []);\n\n    const isSmallMobile = screenTier === \"mobile\";\n    const isTablet = screenTier === \"tablet\";\n    const isMobile = isSmallMobile || isTablet;\n    const isCrowded = items.length >= 7;\n\n    // Responsive physics sizing fallbacks for mobile viewports:\n    // When a large item set (7+ items) is rendered on narrow mobile screens (<480px),\n    // scale resting size down to 28px and gap to 4px so the dock stays neatly within the screen bounds.\n    const effectiveBaseSize = isSmallMobile && isCrowded\n      ? Math.min(baseSize, 28)\n      : isMobile\n      ? Math.min(baseSize, isCrowded ? 34 : 36)\n      : baseSize;\n\n    const effectiveMagnification = isSmallMobile && isCrowded\n      ? Math.min(magnification, 40)\n      : isMobile\n      ? Math.min(magnification, isCrowded ? 48 : 52)\n      : magnification;\n\n    const effectiveDistance = isSmallMobile && isCrowded\n      ? Math.min(distance, 70)\n      : isMobile\n      ? Math.min(distance, 100)\n      : distance;\n\n    const effectiveGap = isSmallMobile && isCrowded\n      ? Math.min(gap, 4)\n      : isMobile\n      ? Math.min(gap, 6)\n      : gap;\n\n    const dynamicHeight = typeof containerHeight === \"number\"\n      ? `${\n          isSmallMobile && isCrowded\n            ? Math.min(containerHeight, 52)\n            : isMobile\n            ? Math.min(containerHeight, 60)\n            : containerHeight\n        }px`\n      : containerHeight;\n\n    const publish = (x: number) => {\n      bus.current.x = x;\n      bus.current.listeners.forEach((listener) => listener(x));\n    };\n\n    return (\n      <div\n        ref={ref}\n        onMouseMove={(e) => {\n          publish(e.clientX);\n          onMouseMove?.(e);\n        }}\n        onMouseLeave={(e) => {\n          publish(Infinity);\n          onMouseLeave?.(e);\n        }}\n        style={{\n          height: dynamicHeight,\n          gap: `${effectiveGap}px`,\n          ...style,\n        }}\n        className={cn(\n          \"mx-auto flex items-end justify-center rounded-2xl shadow-2xl transition-all duration-300\",\n          isSmallMobile && isCrowded\n            ? \"px-1.5 pt-1 pb-0.5\"\n            : \"px-2 sm:px-4 pt-1 sm:pt-2.5 pb-0.5 sm:pb-2.5\",\n          \"bg-card/70 backdrop-blur-xl border border-border/80\",\n          \"ring-1 ring-foreground/5\",\n          \"w-fit max-w-[calc(100%-0.5rem)] sm:max-w-full overflow-visible\",\n          className,\n        )}\n        {...props}\n      >\n        {items.map((item) => (\n          <DockIcon\n            key={item.id}\n            item={item}\n            busRef={bus}\n            dotRef={dot}\n            baseSize={effectiveBaseSize}\n            magnification={effectiveMagnification}\n            distance={effectiveDistance}\n          />\n        ))}\n      </div>\n    );\n  },\n);\n\nDock.displayName = \"Dock\";\n\ninterface DockIconProps {\n  item: DockItem;\n  busRef: Held<PointerBus>;\n  dotRef: Held<DotMemory>;\n  baseSize: number;\n  magnification: number;\n  distance: number;\n}\n\nconst DockIcon: React.FC<DockIconProps> = ({\n  item,\n  busRef,\n  dotRef,\n  baseSize,\n  magnification,\n  distance,\n}) => {\n  const iconRef = useRef<HTMLButtonElement>(null);\n  const tooltipRef = useRef<HTMLDivElement>(null);\n  const [isHovered, setIsHovered] = useState(false);\n\n  // Size follows the pointer through a spring, every frame, without React:\n  // distance from the icon's centre maps [-distance, 0, distance] onto\n  // [base, magnified, base] (clamped), and the spring chases that target.\n  const sizing = useRef({ baseSize, magnification, distance });\n  // Synced after every commit (before the effects below read it), not in render.\n  useLayoutEffect(() => {\n    sizing.current = { baseSize, magnification, distance };\n  });\n  const retarget = useRef<() => void>(() => {});\n\n  useLayoutEffect(() => {\n    const icon = iconRef.current;\n    const bus = busRef.current;\n    if (!icon) return;\n\n    const apply = (size: number) => {\n      icon.style.width = `${size}px`;\n      icon.style.height = `${size}px`;\n    };\n    const target = (x: number) => {\n      const { baseSize: base, magnification: peak, distance: reach } = sizing.current;\n      if (!Number.isFinite(x)) return base;\n      const bounds = icon.getBoundingClientRect();\n      const offset = Math.abs(x - bounds.x - bounds.width / 2);\n      if (offset >= reach) return base;\n      return peak + (base - peak) * (offset / reach);\n    };\n\n    const spring = createSpringFollower(target(bus.x), apply, {\n      ...SIZE_SPRING,\n      ticker: gsapTicker,\n    });\n    apply(spring.get());\n\n    const listener = (x: number) => spring.set(target(x));\n    retarget.current = () => listener(bus.x);\n    bus.listeners.add(listener);\n    return () => {\n      bus.listeners.delete(listener);\n      spring.destroy();\n    };\n  }, [busRef]);\n\n  // New sizes (the mobile fallbacks kicking in) spring over from where it is.\n  useEffect(() => {\n    retarget.current();\n  }, [baseSize, magnification, distance]);\n\n  // Tooltip: stays mounted, so its exit can play before it hides.\n  useLayoutEffect(() => {\n    const tip = tooltipRef.current;\n    if (!tip) return;\n    gsap.killTweensOf(tip);\n    if (isHovered) {\n      gsap.fromTo(\n        tip,\n        { autoAlpha: 0, y: 10, scale: 0.8 },\n        { autoAlpha: 1, y: 0, scale: 1, duration: 0.15, ease: TOOLTIP_EASE },\n      );\n    } else if (gsap.getProperty(tip, \"opacity\") !== 0) {\n      gsap.to(tip, { autoAlpha: 0, y: 5, scale: 0.8, duration: 0.15, ease: TOOLTIP_EASE });\n    }\n  }, [isHovered]);\n\n  return (\n    <div className=\"relative flex flex-col items-center shrink-0\">\n      {/* Floating Tooltip (Renders on all devices on hover/touch) */}\n      <div\n        ref={tooltipRef}\n        aria-hidden={!isHovered}\n        style={{ opacity: 0, visibility: \"hidden\" }}\n        className=\"absolute -top-6 sm:-top-9 px-1.5 py-0.5 rounded text-[8px] sm:text-xs font-semibold whitespace-nowrap pointer-events-none shadow-md bg-primary text-primary-foreground border border-foreground z-50\"\n      >\n        {item.label}\n      </div>\n\n      {/* Interactive Dock Button */}\n      <button\n        ref={iconRef}\n        style={{ width: baseSize, height: baseSize }}\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n        onClick={() => {\n          if (item.onClick) item.onClick();\n          setIsHovered(false);\n        }}\n        aria-label={item.label}\n        className={cn(\n          \"relative flex items-center justify-center rounded-xl shadow-lg transition-colors cursor-pointer select-none shrink-0\",\n          \"bg-gradient-to-t from-muted to-card\",\n          \"border border-border/80 hover:border-foreground/30\",\n          \"text-foreground active:scale-95\",\n        )}\n      >\n        {/* Notification Badge */}\n        {item.badge && (\n          <span className=\"absolute -top-1 -right-1 flex h-3.5 w-3.5 sm:h-4 sm:min-w-4 items-center justify-center rounded-full bg-rose-500 px-1 text-[9px] sm:text-[10px] font-semibold text-white shadow-sm border border-card animate-pulse\">\n            {item.badge}\n          </span>\n        )}\n\n        {/* Scaled Icon Container */}\n        <div className=\"flex items-center justify-center w-3/5 h-3/5 pointer-events-none\">\n          {item.icon}\n        </div>\n      </button>\n\n      {/* Active Dot Indicator */}\n      <div className=\"h-1 sm:h-1.5 w-full flex items-center justify-center mt-1\">\n        {item.isActive && <ActiveDot memoryRef={dotRef} />}\n      </div>\n    </div>\n  );\n};\n\n/**\n * The running-app dot. Only the active icon renders one, so when the active\n * app changes the new dot flies in from where the old one sat — the shared\n * layout animation, on Motion's default layout curve (0.45s, [0.4, 0, 0.1, 1]).\n */\nfunction ActiveDot({ memoryRef }: { memoryRef: Held<DotMemory> }) {\n  const ref = useRef<HTMLDivElement>(null);\n\n  useLayoutEffect(() => {\n    const el = ref.current;\n    const memory = memoryRef.current;\n    if (!el) return;\n    const rect = el.getBoundingClientRect();\n    const from = memory.rect;\n    if (from && (from.x !== rect.x || from.y !== rect.y)) {\n      gsap.fromTo(\n        el,\n        { x: from.x - rect.x, y: from.y - rect.y },\n        { x: 0, y: 0, duration: LAYOUT_TRANSITION.duration, ease: DOT_EASE },\n      );\n    }\n    memory.rect = rect;\n    return () => {\n      // Remember where it left from, including mid-flight, for the next dot.\n      memory.rect = el.getBoundingClientRect();\n    };\n  }, [memoryRef]);\n\n  return (\n    <div\n      ref={ref}\n      className=\"w-1 h-1 sm:w-1.5 sm:h-1.5 rounded-full bg-primary shadow-sm\"\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/DockBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/gsap-motion.ts",
      "content": "import gsap from \"gsap\";\nimport { cubicBezier, type FrameTicker } from \"./motion-curves\";\n\n/**\n * GSAP-side glue for the GSAP builds of components that also ship a Motion\n * Motion twin. Kept apart from motion-curves.ts so the library-free curves\n * never pull GSAP into a CSS or Motion build.\n */\n\n/** `gsap.ticker` as a FrameTicker, so spring followers run on GSAP's loop. */\nexport const gsapTicker: FrameTicker = {\n  add(callback) {\n    const tick = (_time: number, deltaTime: number) => callback(deltaTime);\n    gsap.ticker.add(tick);\n    return () => gsap.ticker.remove(tick);\n  },\n};\n\n/** A Motion / CSS cubic-bezier array as a GSAP ease. */\nexport const bezierEase = (curve: readonly [number, number, number, number] | readonly number[]) =>\n  cubicBezier(curve[0], curve[1], curve[2], curve[3]);\n\n/** `(prefers-reduced-motion: reduce)` right now. */\nexport const prefersReducedMotion = () =>\n  typeof window !== \"undefined\" &&\n  window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches === true;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/gsap-motion.ts"
    },
    {
      "path": "src/components/custom/blocks/motion-curves.ts",
      "content": "/**\n * Motion curves with no animation library behind them.\n *\n * The components here ship as CSS, GSAP or Motion builds that must move\n * identically, so the curves are computed once, from the same numbers, and\n * handed to whichever engine runs them:\n *\n *   - `cubicBezier(x1, y1, x2, y2)` → an easing function (GSAP accepts any\n *     `(progress) => value` as `ease`), exact for Motion's `ease: [..]` arrays\n *     and for CSS `cubic-bezier()`.\n *   - `spring({ stiffness, damping, mass })` → the analytic solution Motion\n *     Motion uses, as an easing function plus the time it takes to settle, or\n *     as a CSS `linear()` string for a fixed start and end.\n *   - `createSpringFollower` → a value that chases a moving target through the\n *     same spring every frame, like Motion's `useSpring`, for pointer-driven\n *     motion that no fixed curve can describe.\n */\n\n// ---------------------------------------------------------------------------\n// Cubic bezier\n// ---------------------------------------------------------------------------\n\n/**\n * The CSS / Motion cubic-bezier as a function of progress. Newton-Raphson\n * with a bisection fallback, the same solver browsers use.\n */\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number) {\n  if (x1 === y1 && x2 === y2) return (t: number) => t;\n\n  const a = (p1: number, p2: number) => 1 - 3 * p2 + 3 * p1;\n  const b = (p1: number, p2: number) => 3 * p2 - 6 * p1;\n  const c = (p1: number) => 3 * p1;\n  const at = (t: number, p1: number, p2: number) =>\n    ((a(p1, p2) * t + b(p1, p2)) * t + c(p1)) * t;\n  const slope = (t: number, p1: number, p2: number) =>\n    3 * a(p1, p2) * t * t + 2 * b(p1, p2) * t + c(p1);\n\n  const solveX = (x: number) => {\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const s = slope(t, x1, x2);\n      if (Math.abs(s) < 1e-6) break;\n      const err = at(t, x1, x2) - x;\n      if (Math.abs(err) < 1e-7) return t;\n      t -= err / s;\n    }\n    let lo = 0;\n    let hi = 1;\n    t = x;\n    for (let i = 0; i < 30 && hi - lo > 1e-7; i++) {\n      const value = at(t, x1, x2);\n      if (value < x) lo = t;\n      else hi = t;\n      t = (lo + hi) / 2;\n    }\n    return t;\n  };\n\n  return (x: number) => (x <= 0 ? 0 : x >= 1 ? 1 : at(solveX(x), y1, y2));\n}\n\n/** Motion's named eases, as the cubic-beziers they are. */\nexport const MOTION_EASE = {\n  linear: [0, 0, 1, 1],\n  easeIn: [0.42, 0, 1, 1],\n  easeOut: [0, 0, 0.58, 1],\n  easeInOut: [0.42, 0, 0.58, 1],\n  circIn: [0.55, 0, 1, 0.45],\n  circOut: [0.55, 0, 0.45, 1],\n  circInOut: [0.85, 0, 0.15, 1],\n  backIn: [0.31, 0.01, 0.66, -0.59],\n  backOut: [0.33, 1.53, 0.69, 0.99],\n  anticipate: [0.66, -0.56, 0.34, 1.35],\n} as const;\n\n/**\n * Motion's default layout transition (`layout` / `layoutId`):\n * `{ duration: 0.45, ease: [0.4, 0, 0.1, 1] }`.\n */\nexport const LAYOUT_TRANSITION = { duration: 0.45, ease: [0.4, 0, 0.1, 1] as const };\n\n// ---------------------------------------------------------------------------\n// Spring\n// ---------------------------------------------------------------------------\n\nexport interface SpringOptions {\n  /** Motion default 100. */\n  stiffness?: number;\n  /** Motion default 10. */\n  damping?: number;\n  /** Motion default 1. */\n  mass?: number;\n  /** Starting velocity, in units of the whole distance per second. */\n  velocity?: number;\n  /** How close to the end counts as settled, as a fraction of the distance. */\n  restDelta?: number;\n}\n\n/**\n * Normalised position (0 → 1) of a spring released from 0 towards 1, at time\n * `t` seconds. The closed-form solution Motion's `spring()` generator\n * uses for the under-, critically and over-damped cases.\n */\nexport function springAt(options: SpringOptions = {}) {\n  const { stiffness = 100, damping = 10, mass = 1, velocity = 0 } = options;\n  const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n  const omega0 = Math.sqrt(stiffness / mass);\n  // Motion's convention: distance still to cover, velocity toward it negated.\n  const delta = 1;\n  const v0 = -velocity;\n\n  if (zeta < 1) {\n    const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);\n    return (t: number) => {\n      const envelope = Math.exp(-zeta * omega0 * t);\n      return (\n        1 -\n        envelope *\n          (((v0 + zeta * omega0 * delta) / omegaD) * Math.sin(omegaD * t) +\n            delta * Math.cos(omegaD * t))\n      );\n    };\n  }\n  if (zeta === 1) {\n    return (t: number) => 1 - Math.exp(-omega0 * t) * (delta + (v0 + omega0 * delta) * t);\n  }\n  const omegaD = omega0 * Math.sqrt(zeta * zeta - 1);\n  return (t: number) => {\n    const envelope = Math.exp(-zeta * omega0 * t);\n    const freq = Math.min(omegaD * t, 300);\n    return (\n      1 -\n      (envelope *\n        ((v0 + zeta * omega0 * delta) * Math.sinh(freq) + omegaD * delta * Math.cosh(freq))) /\n        omegaD\n    );\n  };\n}\n\n/** Seconds until the spring stays within `restDelta` of its end. */\nexport function springDuration(options: SpringOptions = {}) {\n  const at = springAt(options);\n  const restDelta = options.restDelta ?? 0.001;\n  const step = 1 / 240;\n  let lastOutside = 0;\n  for (let t = 0; t <= 10; t += step) {\n    if (Math.abs(1 - at(t)) > restDelta) lastOutside = t;\n    else if (t - lastOutside > 0.1) break;\n  }\n  return Math.max(lastOutside + step, step);\n}\n\n/**\n * A spring as a duration and an easing function over it — what GSAP takes:\n *\n *   const { duration, ease } = springEase({ stiffness: 300, damping: 20 });\n *   gsap.to(el, { y: 0, duration, ease });\n *\n * Exact for a fixed start and end; a spring that has to react mid-flight to a\n * new target needs `createSpringFollower` instead.\n */\nexport function springEase(options: SpringOptions = {}) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  return {\n    duration,\n    ease: (progress: number) => (progress >= 1 ? 1 : at(progress * duration)),\n  };\n}\n\n/**\n * A spring as a CSS `linear()` easing and its duration in ms, for\n * `transition` / `animation` with a fixed start and end.\n *\n *   const { easing, duration } = springLinear({ stiffness: 400, damping: 30 });\n *   el.style.transition = `transform ${duration}ms ${easing}`;\n */\nexport function springLinear(options: SpringOptions = {}, pointsPerSecond = 60) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  const count = Math.max(Math.ceil(duration * pointsPerSecond), 8);\n  const points: string[] = [];\n  for (let i = 0; i <= count; i++) {\n    const t = (i / count) * duration;\n    points.push(String(Math.round((i === count ? 1 : at(t)) * 10000) / 10000));\n  }\n  return { easing: `linear(${points.join(\", \")})`, duration: Math.round(duration * 1000) };\n}\n\n// ---------------------------------------------------------------------------\n// Spring follower (useSpring equivalent)\n// ---------------------------------------------------------------------------\n\n/** Something that calls back once per frame with the time since the last, in ms. */\nexport interface FrameTicker {\n  add: (callback: (deltaMs: number) => void) => () => void;\n}\n\n/** requestAnimationFrame as a ticker — the library-free default. */\nexport const rafTicker: FrameTicker = {\n  add(callback) {\n    let last = performance.now();\n    let frame = requestAnimationFrame(function tick(now) {\n      callback(now - last);\n      last = now;\n      frame = requestAnimationFrame(tick);\n    });\n    return () => cancelAnimationFrame(frame);\n  },\n};\n\nexport interface SpringFollower {\n  /** Chase a new target from wherever the value is now, keeping its velocity. */\n  set: (target: number) => void;\n  /** Move to a value instantly, with no motion. */\n  jump: (value: number) => void;\n  get: () => number;\n  destroy: () => void;\n}\n\n/**\n * A value that follows its target through a spring, integrating the same\n * equation of motion Motion's `useSpring` solves — so a pointer-driven spring\n * reads the same in every build. Runs only while moving; `onUpdate` receives\n * each new value.\n */\nexport function createSpringFollower(\n  initial: number,\n  onUpdate: (value: number) => void,\n  options: SpringOptions & { restSpeed?: number; ticker?: FrameTicker } = {},\n): SpringFollower {\n  const { stiffness = 100, damping = 10, mass = 1, ticker = rafTicker } = options;\n  const restDelta = options.restDelta ?? 0.01;\n  const restSpeed = options.restSpeed ?? 0.01;\n  let value = initial;\n  let target = initial;\n  let velocity = 0;\n  let stop: (() => void) | null = null;\n\n  const step = (deltaMs: number) => {\n    // Fixed 1ms sub-steps keep the integration stable for stiff springs and\n    // independent of the frame rate.\n    const steps = Math.min(Math.max(Math.round(deltaMs), 1), 250);\n    for (let i = 0; i < steps; i++) {\n      const force = -stiffness * (value - target) - damping * velocity;\n      velocity += (force / mass) * 0.001;\n      value += velocity * 0.001;\n    }\n    if (Math.abs(target - value) < restDelta && Math.abs(velocity) < restSpeed) {\n      value = target;\n      velocity = 0;\n      stop?.();\n      stop = null;\n    }\n    onUpdate(value);\n  };\n\n  return {\n    set(next) {\n      target = next;\n      if (!stop && value !== target) stop = ticker.add(step);\n    },\n    jump(next) {\n      stop?.();\n      stop = null;\n      value = target = next;\n      velocity = 0;\n      onUpdate(value);\n    },\n    get: () => value,\n    destroy() {\n      stop?.();\n      stop = null;\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/motion-curves.ts"
    }
  ],
  "docs": "Named exports: Dock, DockItem. `items` take { id, label, icon, onClick?, isActive?, badge? }. `magnification` sets peak scale and `distance` the falloff radius. Pointer-driven — on touch devices it degrades to a plain icon row.",
  "categories": [
    "navigation"
  ],
  "type": "registry:component"
}
