{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elastic-tabs",
  "type": "registry:ui",
  "title": "Elastic Tabs",
  "description": "A tab group whose pill stretches to cover both tabs before it contracts onto the one you picked.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/loomui/elastic-tabs.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface ElasticTabsItem {\n  value: string\n  label: React.ReactNode\n}\n\nexport interface ElasticTabsProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"onChange\"\n> {\n  /** The tabs, in order. */\n  items: ElasticTabsItem[]\n  /** Selected tab. Leave it out to let the group own the selection. */\n  value?: string\n  /** Starting tab when the group owns the selection. */\n  defaultValue?: string\n  /** Called with the value of the tab moved to. */\n  onValueChange?: (value: string) => void\n  /** Length of the whole travel, in milliseconds. */\n  duration?: number\n}\n\nconst EMPTY = { left: 0, width: 0 }\n\nexport function ElasticTabs({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  duration = 280,\n  className,\n  ...props\n}: ElasticTabsProps) {\n  const [own, setOwn] = React.useState(defaultValue ?? items[0]?.value)\n  const isControlled = value !== undefined\n  const active = isControlled ? value : own\n  const index = Math.max(\n    items.findIndex((item) => item.value === active),\n    0\n  )\n\n  const listRef = React.useRef<HTMLDivElement>(null)\n  const tabsRef = React.useRef<(HTMLButtonElement | null)[]>([])\n  const [pill, setPill] = React.useState(EMPTY)\n  // The first placement is a measurement, not a move, so it must not travel.\n  const [settled, setSettled] = React.useState(false)\n\n  const measure = React.useCallback((position: number) => {\n    const tab = tabsRef.current[position]\n    return tab ? { left: tab.offsetLeft, width: tab.offsetWidth } : EMPTY\n  }, [])\n\n  const indexRef = React.useRef(index)\n  indexRef.current = index\n  const pillRef = React.useRef(EMPTY)\n  React.useEffect(() => {\n    pillRef.current = pill\n  }, [pill])\n\n  // Placement. Runs once on mount and again whenever the row is resized, so a\n  // wrapped or re-laid-out group never leaves the pill behind.\n  React.useEffect(() => {\n    const list = listRef.current\n    if (!list) {\n      return\n    }\n\n    const observer = new ResizeObserver(() =>\n      setPill(measure(indexRef.current))\n    )\n    observer.observe(list)\n\n    const timer = window.setTimeout(() => setSettled(true), 0)\n    return () => {\n      observer.disconnect()\n      window.clearTimeout(timer)\n    }\n  }, [measure])\n\n  // Travel. The pill first stretches to cover both tabs, then contracts onto\n  // the new one. Moving `left` and `width` straight to the target slides a\n  // fixed shape across. Spanning first is what makes it read as elastic.\n  React.useEffect(() => {\n    const target = measure(index)\n    const from = pillRef.current\n\n    if (from.width === 0) {\n      setPill(target)\n      return\n    }\n\n    const start = Math.min(from.left, target.left)\n    const end = Math.max(from.left + from.width, target.left + target.width)\n    setPill({ left: start, width: end - start })\n\n    const timer = window.setTimeout(\n      () => setPill(measure(index)),\n      duration * 0.4\n    )\n    return () => window.clearTimeout(timer)\n  }, [index, measure, duration])\n\n  const select = (position: number) => {\n    const next = items[position]\n    if (!next || next.value === active) {\n      return\n    }\n\n    if (!isControlled) {\n      setOwn(next.value)\n    }\n    onValueChange?.(next.value)\n  }\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    let position: number\n\n    if (event.key === \"ArrowLeft\") {\n      position = (index - 1 + items.length) % items.length\n    } else if (event.key === \"ArrowRight\") {\n      position = (index + 1) % items.length\n    } else if (event.key === \"Home\") {\n      position = 0\n    } else if (event.key === \"End\") {\n      position = items.length - 1\n    } else {\n      return\n    }\n\n    event.preventDefault()\n    select(position)\n    // Selection carries focus with it, or the next arrow key would start from\n    // the tab that has just been left behind.\n    tabsRef.current[position]?.focus()\n  }\n\n  return (\n    <div\n      ref={listRef}\n      data-slot=\"elastic-tabs\"\n      role=\"tablist\"\n      onKeyDown={handleKeyDown}\n      className={cn(\n        // Never wider than its parent. The pill is positioned in content\n        // coordinates, so it scrolls with the tabs rather than detaching.\n        \"bg-muted relative inline-flex max-w-full items-center overflow-x-auto rounded-full p-1\",\n        className\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"bg-background absolute inset-y-1 rounded-full shadow-sm\",\n          settled &&\n            \"transition-[left,width] ease-[var(--ease-out-quart)] motion-reduce:transition-none\"\n        )}\n        style={{\n          left: pill.left,\n          width: pill.width,\n          transitionDuration: `${duration * 0.62}ms`,\n        }}\n      />\n\n      {items.map((item, position) => (\n        <button\n          key={item.value}\n          ref={(node) => {\n            tabsRef.current[position] = node\n          }}\n          type=\"button\"\n          role=\"tab\"\n          aria-selected={position === index}\n          // Only the selected tab is in the tab order. The arrows move between\n          // them, which is how a tablist is meant to be walked.\n          tabIndex={position === index ? 0 : -1}\n          onClick={() => select(position)}\n          className={cn(\n            \"text-muted-foreground relative z-10 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-colors outline-none sm:px-4 sm:text-sm\",\n            \"focus-visible:ring-ring/60 focus-visible:ring-2\",\n            position === index && \"text-foreground\"\n          )}\n        >\n          {item.label}\n        </button>\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "ease-out-quart": "cubic-bezier(0.165, 0.84, 0.44, 1)"
    }
  }
}