{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <edu@smoothui.dev>","css":{},"dependencies":["motion"],"devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  useReducedMotion,\n} from \"motion/react\";\nimport type {\n  ItemRect,\n  UseFluidHoverReturn,\n} from \"@/components/smoothui/lib/use-fluid-hover\";\nimport { spring } from \"@/components/smoothui/lib/springs\";\n\n/** What the highlight reads off the hook. */\nexport type FluidHoverSource = Pick<\n  UseFluidHoverReturn,\n  \"activeIndex\" | \"itemRects\" | \"isMeasured\" | \"sessionRef\"\n>;\n\ninterface HighlightFromHook {\n  /** Keep list state but show nothing (closed popup). */\n  hidden?: boolean;\n  hover: FluidHoverSource;\n  rect?: never;\n  session?: never;\n}\n\ninterface HighlightFromRect {\n  hidden?: never;\n  hover?: never;\n  /** Rect in the container's coordinate space; `null` fades out. */\n  rect: ItemRect | null;\n  session: number;\n}\n\nexport type FluidHoverHighlightProps = (\n  | HighlightFromHook\n  | HighlightFromRect\n) & {\n  className?: string;\n  /** Where a fresh session fades in from (e.g. checked row). */\n  from?: ItemRect | null;\n  /** Positional spring. `false` snaps with no travel. */\n  transition?: Transition | false;\n};\n\nconst fade: Transition = { duration: 0.08 };\nconst snap: Transition = { duration: 0 };\n\nexport const toTarget = (rect: ItemRect) => ({\n  height: rect.height,\n  width: rect.width,\n  x: rect.left,\n  y: rect.top,\n});\n\nexport const resolveHighlightTransition = (\n  transition: Transition | false | undefined,\n  reduceMotion: boolean\n): Transition => {\n  const positional =\n    transition === false || reduceMotion ? snap : (transition ?? spring.fast);\n  return { ...positional, opacity: fade };\n};\n\nexport const resolveHighlightSource = (\n  props: FluidHoverHighlightProps\n): { rect: ItemRect | null; session: number } => {\n  if (props.hover) {\n    const { activeIndex, itemRects, isMeasured, sessionRef } = props.hover;\n    const rect =\n      !props.hidden && isMeasured && activeIndex !== null\n        ? (itemRects[activeIndex] ?? null)\n        : null;\n    return { rect, session: sessionRef.current };\n  }\n  return { rect: props.rect, session: props.session };\n};\n\n/**\n * Absolute fill that springs between `useFluidHover` rects.\n * Container must be `position: relative`. Owns no radius/z-index — pass via className.\n */\nexport const FluidHoverHighlight = (props: FluidHoverHighlightProps) => {\n  const { from, className, transition } = props;\n  const { rect, session } = resolveHighlightSource(props);\n  const reduceMotion = useReducedMotion() ?? false;\n\n  return (\n    <AnimatePresence>\n      {rect ? (\n        <motion.div\n          animate={{ opacity: 1, ...toTarget(rect) }}\n          className={cn(\n            \"pointer-events-none absolute top-0 left-0 bg-foreground/5\",\n            className\n          )}\n          data-slot=\"fluid-hover-highlight\"\n          exit={{ opacity: 0, transition: spring.fast.exit }}\n          initial={{ opacity: 0, ...toTarget(from ?? rect) }}\n          key={session}\n          style={{ position: \"absolute\" }}\n          transition={resolveHighlightTransition(transition, reduceMotion)}\n        />\n      ) : null}\n    </AnimatePresence>\n  );\n};\n\nexport default FluidHoverHighlight;\n","path":"index.tsx","target":"components/smoothui/fluid-hover-highlight/index.tsx","type":"registry:ui"}],"name":"fluid-hover-highlight","registryDependencies":["https://smoothui.dev/r/lib.json"],"title":"Fluid Hover Highlight","type":"registry:ui"}