{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "dock",
  "title": "Dock",
  "type": "registry:ui",
  "description": "macOS-style dock menu with magnification on hover. Items expand as the cursor approaches using a cosine bell curve. Supports an items array (icon + label + handler), magnification scale, base size, tooltips on hover, click handlers, and an active state indicator.",
  "categories": [
    "navigation"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/dock/dock.ts",
      "content": "import { LitElement, css, html, nothing, type TemplateResult } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport type { IconNode } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n\nexport interface DockItem {\n  id: string\n  label: string\n  icon?: IconNode | string\n  handler?: () => void\n  active?: boolean\n}\n\nconst trueByDefault = { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null }\n\n/**\n * <uip-dock> — macOS style desktop app dock with cursor magnification.\n */\nexport class UipDock extends LitElement {\n  static styles = [tailwind, css`:host { display: inline-block; }`]\n\n  static properties = {\n    items: { type: Array },\n    baseSize: { type: Number, attribute: 'base-size' },\n    magnification: { type: Number },\n    distance: { type: Number },\n    orientation: { type: String },\n    showTooltips: { attribute: 'show-tooltips', converter: trueByDefault },\n    mouseX: { state: true },\n    hoveredId: { state: true },\n  }\n\n  items: DockItem[] = []\n  baseSize = 48\n  magnification = 1.6\n  distance = 120\n  orientation: 'horizontal' | 'vertical' = 'horizontal'\n  showTooltips = true\n\n  private mouseX: number | null = null\n  private hoveredId: string | null = null\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'dock')\n    this.setAttribute('data-orientation', this.orientation)\n  }\n\n  private onMouseMove(e: MouseEvent) {\n    this.mouseX = e.clientX\n  }\n\n  private onMouseLeave() {\n    this.mouseX = null\n    this.hoveredId = null\n  }\n\n  private calculateSize(index: number): number {\n    if (this.mouseX === null) return this.baseSize\n    const itemsEls = this.renderRoot?.querySelectorAll<HTMLElement>('[data-slot=\"dock-item\"]')\n    const el = itemsEls?.[index]\n    if (!el) return this.baseSize\n    const rect = el.getBoundingClientRect()\n    const center = rect.left + rect.width / 2\n    const dist = Math.abs(this.mouseX - center)\n    if (dist > this.distance) return this.baseSize\n    const t = 1 - dist / this.distance\n    const scale = 1 + (this.magnification - 1) * t\n    return this.baseSize * scale\n  }\n\n  private handleItemClick(item: DockItem) {\n    item.handler?.()\n    this.dispatchEvent(new CustomEvent('item-click', { detail: { item, id: item.id }, bubbles: true, composed: true }))\n    this.requestUpdate()\n  }\n\n  private renderIcon(itemIcon?: IconNode | string, size = 48): TemplateResult | typeof nothing {\n    if (!itemIcon) return nothing\n    const s = `${size * 0.5}px`\n    if (Array.isArray(itemIcon)) {\n      return html`<span class=\"inline-flex items-center justify-center [&_svg]:size-full\" style=\"width: ${s}; height: ${s};\">${icon(itemIcon as IconNode, 'dock-icon')}</span>`\n    }\n    if (typeof itemIcon === 'string') {\n      return html`<span style=\"width: ${s}; height: ${s};\" .innerHTML=${itemIcon}></span>`\n    }\n    return nothing\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"base\"\n        data-uipkge=\"\"\n        data-slot=\"dock\"\n        data-orientation=${this.orientation}\n        class=\"border-border/60 bg-background/60 flex items-end justify-center gap-3 rounded-2xl border px-3 py-2 backdrop-blur-md\"\n        @mousemove=${this.onMouseMove}\n        @mouseleave=${this.onMouseLeave}\n      >\n        ${this.items.map((item, index) => {\n          const size = this.calculateSize(index)\n          return html`\n            <div\n              data-slot=\"dock-item\"\n              ?data-active=${item.active}\n              role=\"button\"\n              tabindex=\"0\"\n              aria-label=${item.label}\n              aria-current=${item.active ? 'true' : nothing}\n              class=\"group focus-visible:ring-ring/50 relative flex shrink-0 cursor-pointer items-end justify-center rounded-xl outline-none focus-visible:ring-[3px]\"\n              @mouseenter=${() => (this.hoveredId = item.id)}\n              @mouseleave=${() => (this.hoveredId = null)}\n              @click=${() => this.handleItemClick(item)}\n              @keydown=${(e: KeyboardEvent) => {\n                if (e.key === 'Enter' || e.key === ' ') {\n                  e.preventDefault()\n                  this.handleItemClick(item)\n                }\n              }}\n            >\n              ${this.showTooltips && this.hoveredId === item.id\n                ? html`\n                    <span\n                      part=\"tooltip\"\n                      class=\"border-border bg-popover text-popover-foreground pointer-events-none absolute -top-10 left-1/2 -translate-x-1/2 rounded-md border px-2 py-1 text-xs whitespace-nowrap shadow-md\"\n                    >\n                      ${item.label}\n                    </span>\n                  `\n                : nothing}\n\n              <span\n                part=\"tile\"\n                class=${cn(\n                  'flex items-center justify-center rounded-xl border transition-[width,height] duration-100 ease-out will-change-[width,height]',\n                  item.active\n                    ? 'border-primary/40 bg-primary/10 text-primary'\n                    : 'border-border/50 bg-muted/40 text-foreground hover:bg-muted',\n                )}\n                style=${styleMap({ width: `${size}px`, height: `${size}px` })}\n              >\n                ${this.renderIcon(item.icon, size)}\n              </span>\n\n              ${item.active\n                ? html`<span\n                    class=\"bg-primary absolute -bottom-2 size-1 rounded-full\"\n                    aria-hidden=\"true\"\n                  ></span>`\n                : nothing}\n            </div>\n          `\n        })}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-dock') || customElements.define('uip-dock', UipDock)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-dock': UipDock\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/dock/dock.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
