{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drip-tabs",
  "type": "registry:component",
  "title": "Drip Tabs",
  "description": "Vertical settings tabs where the highlight runs down the rail like a bead of water, trailing a little drop that catches up and sinks back in.",
  "categories": [
    "component",
    "Liquid"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/drip-tabs.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity } from \"motion/react\";\nimport { type ReactNode, useCallback, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DripTabItem {\n  value: string;\n  label: string;\n  icon?: ReactNode;\n  content: ReactNode;\n}\n\nexport interface DripTabsProps {\n  items: DripTabItem[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\nconst leadSpring = { stiffness: 480, damping: 36, mass: 0.7 };\nconst trailSpring = { stiffness: 200, damping: 25, mass: 1 };\nconst DROP = 0.55;\n\nexport function DripTabs({\n  items,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  className,\n  \"aria-label\": ariaLabel = \"Sections\",\n}: DripTabsProps) {\n  const filterId = `drip-tabs-${useId().replace(/:/g, \"\")}`;\n  const reduce = useReducedMotion();\n  const [internal, setInternal] = useState(defaultValue ?? items[0]?.value);\n  const value = valueProp ?? internal;\n  const listRef = useRef<HTMLDivElement>(null);\n  const tabRefs = useRef(new Map<string, HTMLElement>());\n  const ready = useRef(false);\n\n  const targetY = useMotionValue(0);\n  const targetH = useMotionValue(0);\n  const leadY = useSpring(targetY, leadSpring);\n  const leadH = useSpring(targetH, leadSpring);\n  const trailY = useSpring(targetY, trailSpring);\n  const trailH = useSpring(targetH, trailSpring);\n\n  const leadV = useVelocity(leadY);\n  const trailV = useVelocity(trailY);\n  const leadThin = useTransform(leadV, (v) => 1 - Math.min(Math.abs(v) / 6000, 0.08));\n  const trailThin = useTransform(trailV, (v) => 0.5 - Math.min(Math.abs(v) / 3000, 0.14));\n  const dropY = useTransform(() => trailY.get() + (trailH.get() * (1 - DROP)) / 2);\n  const dropH = useTransform(trailH, (h) => h * DROP);\n\n  const place = useCallback(\n    (instant: boolean) => {\n      const el = value ? tabRefs.current.get(value) : undefined;\n      if (!el) return;\n      targetY.set(el.offsetTop);\n      targetH.set(el.offsetHeight);\n      if (instant) {\n        for (const [mv, v] of [\n          [leadY, el.offsetTop],\n          [trailY, el.offsetTop],\n          [leadH, el.offsetHeight],\n          [trailH, el.offsetHeight],\n        ] as const)\n          mv.jump(v);\n      }\n    },\n    [value, targetY, targetH, leadY, trailY, leadH, trailH],\n  );\n\n  useLayoutEffect(() => {\n    place(!ready.current || !!reduce);\n    ready.current = true;\n  }, [place, reduce]);\n\n  const placeRef = useRef(place);\n  useLayoutEffect(() => {\n    placeRef.current = place;\n  }, [place]);\n  useLayoutEffect(() => {\n    const list = listRef.current;\n    if (!list) return;\n    let size = `${list.offsetWidth}x${list.offsetHeight}`;\n    const ro = new ResizeObserver(() => {\n      const next = `${list.offsetWidth}x${list.offsetHeight}`;\n      if (next === size) return;\n      size = next;\n      placeRef.current(true);\n    });\n    ro.observe(list);\n    return () => ro.disconnect();\n  }, []);\n\n  const change = (next: string) => {\n    if (valueProp === undefined) setInternal(next);\n    onValueChange?.(next);\n  };\n\n  return (\n    <TabsPrimitive.Root\n      className={cn(\"@container/drip-tabs flex w-full overflow-hidden rounded-2xl border bg-card shadow-xs/5\", className)}\n      onValueChange={(v) => change(v as string)}\n      orientation=\"vertical\"\n      value={value}\n    >\n      <TabsPrimitive.List\n        aria-label={ariaLabel}\n        className=\"relative isolate flex shrink-0 flex-col gap-0.5 border-r p-2\"\n        ref={listRef}\n      >\n        <svg aria-hidden className=\"absolute size-0\">\n          <filter colorInterpolationFilters=\"sRGB\" height=\"140%\" id={filterId} width=\"160%\" x=\"-30%\" y=\"-20%\">\n            <feGaussianBlur in=\"SourceGraphic\" result=\"blur\" stdDeviation=\"6\" />\n            <feColorMatrix in=\"blur\" result=\"goo\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 20 -8\" />\n            <feComposite in=\"SourceGraphic\" in2=\"goo\" operator=\"atop\" />\n          </filter>\n        </svg>\n        {/* An opaque fill (never an alpha token) so the goo threshold leaves clean edges with no fringe. */}\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 -z-10 [--drip-fill:color-mix(in_srgb,var(--color-foreground)_7%,var(--color-card))] dark:[--drip-fill:color-mix(in_srgb,var(--color-foreground)_10%,var(--color-card))]\"\n          style={{ filter: `url(#${filterId})` }}\n        >\n          <motion.span\n            className=\"absolute inset-x-2 top-0 rounded-full bg-(--drip-fill)\"\n            style={{ y: dropY, height: dropH, scaleX: trailThin }}\n          />\n          <motion.span\n            className=\"absolute inset-x-2 top-0 rounded-lg bg-(--drip-fill)\"\n            style={{ y: leadY, height: leadH, scaleX: leadThin }}\n          />\n        </span>\n        {items.map((item) => (\n          <TabsPrimitive.Tab\n            className={cn(\n              \"relative flex h-9 cursor-pointer select-none items-center gap-2 whitespace-nowrap rounded-lg px-2.5 font-medium text-muted-foreground text-sm outline-none transition-colors duration-200 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0\",\n              \"data-active:text-foreground\",\n            )}\n            key={item.value}\n            ref={(el: HTMLElement | null) => {\n              if (el) tabRefs.current.set(item.value, el);\n              else tabRefs.current.delete(item.value);\n            }}\n            value={item.value}\n          >\n            {item.icon}\n            <span className=\"sr-only @[26rem]/drip-tabs:not-sr-only\">{item.label}</span>\n          </TabsPrimitive.Tab>\n        ))}\n      </TabsPrimitive.List>\n      {items.map((item) => (\n        <TabsPrimitive.Panel className=\"min-w-0 flex-1 p-4 outline-none\" key={item.value} value={item.value}>\n          <motion.div\n            animate={{ opacity: 1, y: 0 }}\n            initial={{ opacity: 0, y: 3 }}\n            transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1], y: reduce ? { duration: 0 } : undefined }}\n          >\n            {item.content}\n          </motion.div>\n        </TabsPrimitive.Panel>\n      ))}\n    </TabsPrimitive.Root>\n  );\n}\n"
    }
  ]
}