{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-floating",
  "title": "Floating layer",
  "description": "The motion and material every Eased popup shares: grows from the trigger's transform-origin, enters a few pixels toward the trigger, closes at half the distance and faster, and reverses mid-flight because open and close are transitions. FloatingLayer renders any Base UI popup family (Tooltip, Popover, PreviewCard, Menu) with it.",
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/floating/floating.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ComponentType, CSSProperties, ReactNode } from \"react\";\nimport { duration } from \"../../lib/motion\";\n\n/**\n * The Eased floating layer: one way for every popup (tooltip, popover, hover card, and later\n * menu, select, combobox) to appear and leave. Base UI owns behaviour (focus, dismissal,\n * collision, safe hover); this file owns motion and material.\n *\n *   - Grows from the trigger: `transform-origin` is Base UI's `--transform-origin`.\n *   - Enters from a few pixels toward the trigger, read from `data-side`, so a popup below its\n *     trigger drops down into place and one to the right slides out rightward.\n *   - Opening and closing are CSS transitions on Base UI's `data-starting-style` and\n *     `data-ending-style`, so a close during the open (or the reverse) turns around mid-flight.\n *   - Exits travel half the enter distance, scale half as far, never overshoot, and are faster.\n *   - Reduced motion keeps only a 100ms fade. Tooltips in a delay group swap with no animation.\n */\n\nexport type FloatingSide = \"top\" | \"bottom\" | \"left\" | \"right\" | \"inline-start\" | \"inline-end\";\nexport type FloatingAlign = \"start\" | \"center\" | \"end\";\nexport type FloatingRole = \"tooltip\" | \"popover\" | \"card\";\n\nexport type FloatingMotion = {\n  /** Gap between trigger and popup, px. */\n  sideOffset: number;\n  /** How far the popup travels toward its resting place on enter, px. Exits travel half. */\n  distance: number;\n  /** Scale on enter; exits scale half as far (0.97 → 0.985). Never 0. */\n  scale: number;\n  /** Opacity fade on enter, ms. Transform rides `--eased-spring-snappy`. */\n  fade: number;\n  /** Whole exit, ms, on `--eased-ease-standard`. Keep it ≤ 0.75 × fade. */\n  exit: number;\n  /** Skip the animation when Base UI marks it instant (`data-instant`), e.g. a tooltip delay group. */\n  honourInstant: boolean;\n};\n\nconst ms = (s: number) => Math.round(s * 1000);\n\n/** Per-role motion. Tooltips are smaller and quicker; cards sit a little further from their link. */\nexport const FLOATING_ROLES: Record<FloatingRole, FloatingMotion & { side: FloatingSide }> = {\n  tooltip: { side: \"top\", sideOffset: 6, distance: 3, scale: 0.97, fade: ms(duration.fast), exit: ms(duration.instant), honourInstant: true },\n  popover: { side: \"bottom\", sideOffset: 6, distance: 5, scale: 0.97, fade: ms(duration.standard), exit: ms(duration.fast), honourInstant: false },\n  card: { side: \"bottom\", sideOffset: 8, distance: 5, scale: 0.97, fade: ms(duration.standard), exit: ms(duration.fast), honourInstant: false },\n};\n\n/*\n * Role values arrive as inline `--eased-float-*` properties. The classes copy them into short\n * working variables (`--ef-*`) so the `reduced` variant and `data-instant` can override the copies:\n * an inline style would otherwise beat any class.\n */\nconst MOTION = [\n  \"origin-(--transform-origin)\",\n  \"[--ef-d:var(--eased-float-distance)] [--ef-s:var(--eased-float-scale)] [--ef-in:var(--eased-duration-spring-snappy)] [--ef-fade:var(--eased-float-fade)] [--ef-out:var(--eased-float-exit)]\",\n  // Direction toward the trigger: +1 / −1 per axis, from the side Base UI actually placed it on.\n  \"data-[side=top]:[--ef-y:1] data-[side=bottom]:[--ef-y:-1] data-[side=left]:[--ef-x:1] data-[side=right]:[--ef-x:-1] data-[side=inline-start]:[--ef-x:1] data-[side=inline-end]:[--ef-x:-1] rtl:data-[side=inline-start]:[--ef-x:-1] rtl:data-[side=inline-end]:[--ef-x:1]\",\n  // Open: opacity on the enter curve, transform on the snappy spring.\n  \"[transition-property:opacity,translate,scale] [transition-duration:var(--ef-fade),var(--ef-in),var(--ef-in)] [transition-timing-function:var(--eased-ease-enter),var(--eased-spring-snappy),var(--eased-spring-snappy)]\",\n  \"data-starting-style:opacity-0 data-starting-style:[translate:calc(var(--ef-x,0)*var(--ef-d))_calc(var(--ef-y,0)*var(--ef-d))] data-starting-style:[scale:var(--ef-s)]\",\n  // Close: half the distance, half the scale, one faster duration, no overshoot.\n  \"data-ending-style:opacity-0 data-ending-style:[translate:calc(var(--ef-x,0)*var(--ef-d)*0.5)_calc(var(--ef-y,0)*var(--ef-d)*0.5)] data-ending-style:[scale:calc(0.5_+_var(--ef-s)*0.5)] data-ending-style:[transition-duration:var(--ef-out)] data-ending-style:[transition-timing-function:var(--eased-ease-standard)]\",\n  // Reduced motion: no travel, no scale, a 100ms fade.\n  \"reduced:[--ef-d:0px] reduced:[--ef-s:1] reduced:[--ef-in:0ms] reduced:[--ef-fade:var(--eased-duration-instant)] reduced:[--ef-out:var(--eased-duration-instant)]\",\n].join(\" \");\n\nconst INSTANT = \"data-instant:[--ef-in:0ms] data-instant:[--ef-fade:0ms] data-instant:[--ef-out:0ms]\";\n\n/**\n * Motion classes for any Base UI popup part. Pair with `floatingVars(role)` as its style, or use\n * `<FloatingLayer>`, which does both.\n */\nexport function floatingMotion(honourInstant = false) {\n  return honourInstant ? `${MOTION} ${INSTANT}` : MOTION;\n}\n\n/** The inline custom properties `floatingMotion` reads. */\nexport function floatingVars(motion: Pick<FloatingMotion, \"distance\" | \"scale\" | \"fade\" | \"exit\">): CSSProperties {\n  return {\n    \"--eased-float-distance\": `${motion.distance}px`,\n    \"--eased-float-scale\": motion.scale,\n    \"--eased-float-fade\": `${motion.fade}ms`,\n    \"--eased-float-exit\": `${motion.exit}ms`,\n  } as CSSProperties;\n}\n\n/**\n * Floating material (R12, R25): raised surface, `border-line`, `shadow-floating`, Eased UI type.\n * The popup itself never draws a focus ring; focus moves to its first control, which draws its own.\n */\nexport const floatingSurface = \"border border-line bg-surface-raised text-ui text-text shadow-floating outline-none\";\n\n/**\n * Behaviour classes for anything that anchors a popup (R64): it presses by colour, never by\n * scale, so the popup's origin stays put. Colour moves at 150ms; keyboard focus draws Eased's\n * ring. Add your own look, and use `data-popup-open:` for the open state.\n */\nexport const floatingTrigger =\n  \"[transition:color_150ms_var(--eased-ease-standard),background-color_150ms_var(--eased-ease-standard),border-color_150ms_var(--eased-ease-standard),text-decoration-color_150ms_var(--eased-ease-standard),box-shadow_var(--eased-duration-focus)_var(--eased-ease-standard)] focus-ring cursor-pointer select-none\";\n\n/* ---------- FloatingLayer: Portal → Positioner → Popup for any Base UI popup family ---------- */\n\ntype Part = { className?: string; style?: CSSProperties; children?: ReactNode };\n\n/** The three parts every Base UI popup family exposes: `Tooltip`, `Popover`, `PreviewCard`, `Menu` … */\nexport type FloatingParts = {\n  Portal: ComponentType<{ children?: ReactNode; container?: HTMLElement | null }>;\n  Positioner: ComponentType<\n    Part & { side?: FloatingSide; align?: FloatingAlign; sideOffset?: number; alignOffset?: number; collisionPadding?: number }\n  >;\n  Popup: ComponentType<Part>;\n};\n\ntype Reserved = \"className\" | \"style\" | \"children\" | \"side\" | \"align\" | \"sideOffset\" | \"alignOffset\" | \"collisionPadding\";\n\nexport type FloatingLayerProps<P extends FloatingParts> = {\n  /** The Base UI namespace to render with, e.g. `Popover` from `@base-ui/react/popover`. */\n  parts: P;\n  /** Sets the side, gap and motion defaults. Default \"popover\". */\n  role?: FloatingRole;\n  /** Preferred side; Base UI flips it on collision and the enter direction follows. */\n  side?: FloatingSide;\n  align?: FloatingAlign;\n  /** Gap to the trigger, px. Defaults per role (6, or 8 for cards). */\n  sideOffset?: number;\n  alignOffset?: number;\n  /** Space kept from the viewport edge, px. Default 8. */\n  collisionPadding?: number;\n  /** Override any of the role's motion values. */\n  motion?: Partial<FloatingMotion>;\n  /** Portal target. Default `document.body`. */\n  container?: HTMLElement | null;\n  /** Stacking order of the positioner. Default 50; raise it for a popup inside a higher layer (a dialog). */\n  zIndex?: number;\n  /** Popup classes, appended after the motion classes. Bring your own surface (see `floatingSurface`). */\n  className?: string;\n  style?: CSSProperties;\n  /** Extra props for the Positioner, e.g. `sticky`, `anchor`, `collisionAvoidance`. */\n  positionerProps?: Omit<ComponentProps<P[\"Positioner\"]>, Reserved>;\n  /** Extra props for the Popup, e.g. Popover's `initialFocus` and `finalFocus`. */\n  popupProps?: Omit<ComponentProps<P[\"Popup\"]>, Reserved>;\n  children?: ReactNode;\n};\n\n/**\n * Renders a Base UI popup with Eased's floating motion. Behaviour stays with the family you pass:\n *\n *   <Popover.Root>\n *     <Popover.Trigger className={floatingTrigger}>Share</Popover.Trigger>\n *     <FloatingLayer parts={Popover} className={`${floatingSurface} rounded-panel p-4`}>…</FloatingLayer>\n *   </Popover.Root>\n */\nexport function FloatingLayer<P extends FloatingParts>({\n  parts,\n  role = \"popover\",\n  side,\n  align = \"center\",\n  sideOffset,\n  alignOffset = 0,\n  collisionPadding = 8,\n  motion,\n  container,\n  zIndex = 50,\n  className = \"\",\n  style,\n  positionerProps,\n  popupProps,\n  children,\n}: FloatingLayerProps<P>) {\n  const { Portal, Positioner, Popup } = parts as FloatingParts;\n  const m = { ...FLOATING_ROLES[role], ...motion };\n\n  return (\n    <Portal container={container}>\n      <Positioner\n        {...positionerProps}\n        side={side ?? m.side}\n        align={align}\n        sideOffset={sideOffset ?? m.sideOffset}\n        alignOffset={alignOffset}\n        collisionPadding={collisionPadding}\n        style={{ zIndex }}\n      >\n        <Popup {...popupProps} className={`${floatingMotion(m.honourInstant)} ${className}`} style={{ ...floatingVars(m), ...style }}>\n          {children}\n        </Popup>\n      </Positioner>\n    </Portal>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/floating/floating.tsx"
    }
  ],
  "type": "registry:ui"
}