{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-dock",
  "title": "App Dock",
  "description": "Keyboard-accessible shortcut dock with fine-pointer magnification.",
  "registryDependencies": [
    "https://components.exe.xyz/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "src/components/blocks/app-dock.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\"\n\n/*\n * SPDX-License-Identifier: MIT\n * Inspired by Amicro's Physics Spring Dock by Syed Subhan Uddin.\n * Upstream: https://github.com/Subhan-code/Amicro--Micro-transitions-\n * Source: src/components/css-animations/Dock.tsx\n * Audited revision: 07adc1640084940f045875e2bb1b682c90f30c3c\n * This implementation is an original accessible React/CSS adaptation.\n */\n\nexport type AppDockItem = {\n  id: string\n  label: string\n  icon: React.ReactNode\n  disabled?: boolean\n  onSelect?: () => void\n}\n\nexport type AppDockProps = {\n  items: readonly AppDockItem[]\n  activeId?: string\n  defaultActiveId?: string\n  onActiveChange?: (id: string) => void\n  magnification?: number\n  label?: string\n  className?: string\n}\n\nexport function AppDock({\n  items,\n  activeId,\n  defaultActiveId,\n  onActiveChange,\n  magnification = 1.42,\n  label = \"Application shortcuts\",\n  className = \"\",\n}: AppDockProps) {\n  const dockRef = React.useRef<HTMLDivElement>(null)\n  const frameRef = React.useRef(0)\n  const pointerXRef = React.useRef(0)\n  const reduced = useReducedMotion()\n  const [internalActive, setInternalActive] = React.useState(\n    defaultActiveId ?? items[0]?.id\n  )\n  const selected = activeId ?? internalActive\n\n  const resetItems = React.useCallback(() => {\n    if (frameRef.current) window.cancelAnimationFrame(frameRef.current)\n    frameRef.current = 0\n    dockRef.current\n      ?.querySelectorAll<HTMLElement>(\"[data-dock-item]\")\n      .forEach((item) => {\n        item.style.setProperty(\"--dock-scale\", \"1\")\n        item.style.setProperty(\"--dock-lift\", \"0px\")\n      })\n  }, [])\n\n  React.useEffect(() => {\n    return resetItems\n  }, [resetItems])\n\n  const focusRelative = (current: HTMLButtonElement, delta: number) => {\n    const buttons = Array.from(\n      dockRef.current?.querySelectorAll<HTMLButtonElement>(\n        \"[data-dock-item]:not(:disabled)\"\n      ) ?? []\n    )\n    const index = buttons.indexOf(current)\n    if (index < 0 || buttons.length === 0) return\n    buttons[(index + delta + buttons.length) % buttons.length]?.focus()\n  }\n\n  return (\n    <div\n      ref={dockRef}\n      role=\"toolbar\"\n      aria-label={label}\n      data-app-dock=\"\"\n      className={`flex min-h-16 w-fit max-w-full items-end gap-1.5 rounded-2xl border border-border bg-card/95 p-2 shadow-xl shadow-black/10 backdrop-blur ${className}`}\n      onPointerMove={(event) => {\n        if (reduced || event.pointerType !== \"mouse\") return\n        pointerXRef.current = event.clientX\n        if (frameRef.current) return\n        frameRef.current = window.requestAnimationFrame(() => {\n          frameRef.current = 0\n          const radius = 82\n          dockRef.current\n            ?.querySelectorAll<HTMLElement>(\"[data-dock-item]\")\n            .forEach((item) => {\n              const rect = item.getBoundingClientRect()\n              const distance = Math.abs(\n                pointerXRef.current - (rect.left + rect.width / 2)\n              )\n              const proximity = Math.max(0, 1 - distance / radius)\n              const scale = 1 + (magnification - 1) * proximity * proximity\n              item.style.setProperty(\"--dock-scale\", scale.toFixed(3))\n              item.style.setProperty(\n                \"--dock-lift\",\n                `${-8 * proximity * proximity}px`\n              )\n            })\n        })\n      }}\n      onPointerLeave={resetItems}\n    >\n      <style>{`\n        [data-dock-item] {\n          transform: translateY(var(--dock-lift, 0px)) scale(var(--dock-scale, 1));\n          transform-origin: 50% 100%;\n          transition: transform 150ms cubic-bezier(.16,1,.3,1), background-color 150ms ease, color 150ms ease;\n          will-change: transform;\n        }\n        [data-dock-label] {\n          opacity: 0;\n          transform: translate(-50%, 4px);\n          transition: opacity 100ms ease, transform 140ms cubic-bezier(.16,1,.3,1);\n        }\n        [data-dock-item]:hover [data-dock-label],\n        [data-dock-item]:focus-visible [data-dock-label] {\n          opacity: 1;\n          transform: translate(-50%, 0);\n        }\n        @media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) {\n          [data-dock-item] { transform: none !important; }\n        }\n      `}</style>\n      {items.map((item) => {\n        const isActive = item.id === selected\n        return (\n          <button\n            key={item.id}\n            type=\"button\"\n            data-dock-item=\"\"\n            aria-label={item.label}\n            aria-pressed={isActive}\n            disabled={item.disabled}\n            className=\"relative grid size-11 shrink-0 place-items-center rounded-xl border border-border bg-background text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-40 aria-pressed:border-primary aria-pressed:bg-primary aria-pressed:text-primary-foreground\"\n            onClick={() => {\n              setInternalActive(item.id)\n              onActiveChange?.(item.id)\n              item.onSelect?.()\n            }}\n            onKeyDown={(event) => {\n              if (event.key === \"ArrowRight\") {\n                event.preventDefault()\n                focusRelative(event.currentTarget, 1)\n              } else if (event.key === \"ArrowLeft\") {\n                event.preventDefault()\n                focusRelative(event.currentTarget, -1)\n              } else if (event.key === \"Home\" || event.key === \"End\") {\n                event.preventDefault()\n                const buttons = Array.from(\n                  dockRef.current?.querySelectorAll<HTMLButtonElement>(\n                    \"[data-dock-item]:not(:disabled)\"\n                  ) ?? []\n                )\n                buttons[event.key === \"Home\" ? 0 : buttons.length - 1]?.focus()\n              }\n            }}\n          >\n            <span aria-hidden=\"true\">{item.icon}</span>\n            <span\n              data-dock-label=\"\"\n              className=\"pointer-events-none absolute bottom-[calc(100%+10px)] left-1/2 z-10 rounded-md bg-foreground px-2 py-1 text-[10px] font-medium whitespace-nowrap text-background shadow-lg\"\n            >\n              {item.label}\n            </span>\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "src/components/blocks/app-dock.tsx"
    }
  ],
  "meta": {
    "collection": "motion-effects",
    "license": "MIT",
    "usage": "<AppDock\n  items={[\n    { id: \"home\", label: \"Home\", icon: <span>H</span> },\n    { id: \"search\", label: \"Search\", icon: <span>S</span> },\n  ]}\n/>",
    "source": "https://github.com/Subhan-code/Amicro--Micro-transitions-",
    "sourcePath": "src/components/css-animations/Dock.tsx",
    "sourceRevision": "07adc1640084940f045875e2bb1b682c90f30c3c"
  },
  "type": "registry:block"
}