{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "floating-panel",
  "title": "Floating Panel",
  "description": "A movable window that stays on screen: drag with pointer capture, resize, minimise, close and move by keyboard, as a titled panel, tight chrome, a side dock or a tool palette.",
  "dependencies": [
    "lucide-react",
    "motion",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/floating-panel.tsx",
      "content": "import type * as React from \"react\"\nimport { useId, useRef } from \"react\"\nimport { Portal } from \"radix-ui\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport type { DockSide, PanelPoint, PanelSize } from \"@/registry/miseui/ui/floating-panel-math\"\nimport {\n  PanelHeader,\n  ResizeHandle,\n  surfaceClass,\n  type FloatingPanelLabels,\n  type FloatingPanelVariant,\n} from \"@/registry/miseui/ui/floating-panel-parts\"\nimport { usePanelGestures } from \"@/registry/miseui/ui/floating-panel-gestures\"\nimport { useControllable, useFitWhenShown, usePanelDismiss, usePanelGeometry, type PanelGeometry } from \"@/registry/miseui/ui/floating-panel-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype FloatingPanelEffect = \"momentum\"\n\nconst INSTANT = { duration: 0 } as const\nconst DEFAULT_MIN_SIZE: PanelSize = { width: 160, height: 96 }\nconst DEFAULT_SIZE: Record<FloatingPanelVariant, PanelSize> = {\n  panel: { width: 288, height: 240 },\n  compact: { width: 216, height: 160 },\n  docked: { width: 240, height: 240 },\n  tool: { width: 48, height: 208 },\n}\nconst DEFAULT_LABELS: FloatingPanelLabels = {\n  move: (title) => `Move ${title}`,\n  resize: (title) => `Resize ${title}`,\n  minimize: \"Minimise\",\n  restore: \"Restore\",\n  close: \"Close\",\n  hint: \"Use the arrow keys. Hold Shift for larger steps.\",\n}\n\ntype RootAttributes = Omit<\n  React.ComponentProps<\"div\">,\n  \"title\" | \"style\" | \"onKeyDown\" | \"onFocusCapture\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\"\n>\n\ninterface FloatingPanelProps extends RootAttributes {\n  /** Names the dialog. `tool` keeps it for assistive tech and shows only a grip. */\n  title: string\n  /** `panel` a titled window, `compact` tight chrome, `docked` snaps to the nearest side at full height, `tool` a narrow palette. */\n  variant?: FloatingPanelVariant\n  /** `momentum` keeps a thrown panel travelling after release, and it still stops at the edges. */\n  effect?: FloatingPanelEffect\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  minimized?: boolean\n  defaultMinimized?: boolean\n  onMinimizedChange?: (minimized: boolean) => void\n  /** The element to stay inside, which must be positioned. Without one the panel floats over the viewport through a portal. */\n  boundary?: React.RefObject<HTMLElement | null>\n  defaultPosition?: PanelPoint\n  onPositionChange?: (position: PanelPoint) => void\n  defaultSize?: PanelSize\n  minSize?: PanelSize\n  maxSize?: Partial<PanelSize>\n  onSizeChange?: (size: PanelSize) => void\n  /** On by default, except `tool`, which is as large as its contents. */\n  resizable?: boolean\n  /** Hides the close button and turns Escape off, for a panel that cannot go away. */\n  closable?: boolean\n  labels?: Partial<FloatingPanelLabels>\n}\n\ninterface PanelBodyProps {\n  variant: FloatingPanelVariant\n  minimized: boolean\n  fullHeight: boolean\n  reduceMotion: boolean\n  geometry: PanelGeometry\n  children: React.ReactNode\n}\n\nfunction PanelBody({ variant, minimized, fullHeight, reduceMotion, geometry, children }: PanelBodyProps) {\n  const fixedHeight = variant !== \"tool\" && variant !== \"docked\"\n  return (\n    <motion.div\n      data-slot=\"floating-panel-collapse\"\n      inert={minimized}\n      className={cn(\"min-h-0 overflow-hidden\", fullHeight && \"flex-1\")}\n      initial={false}\n      animate={{ height: minimized ? 0 : \"auto\" }}\n      transition={reduceMotion ? INSTANT : SPRING_DEFAULT}\n      onAnimationComplete={() => geometry.fit()}\n    >\n      <motion.div\n        data-slot=\"floating-panel-body\"\n        style={fixedHeight ? { height: geometry.height } : undefined}\n        className={cn(\"overflow-auto\", variant !== \"tool\" && \"text-sm\", variant === \"docked\" && \"h-full\")}\n      >\n        {children}\n      </motion.div>\n    </motion.div>\n  )\n}\n\nfunction panelFrameClass(variant: FloatingPanelVariant, side: DockSide, inBoundary: boolean, fullHeight: boolean, className?: string) {\n  return cn(\n    inBoundary ? \"absolute\" : \"fixed\",\n    \"top-0 left-0 z-40 flex flex-col overflow-hidden outline-none\",\n    fullHeight && (inBoundary ? \"h-full\" : \"h-dvh\"),\n    surfaceClass(variant, side),\n    className,\n  )\n}\n\nfunction FloatingPanel({\n  title, variant = \"panel\", effect, open, defaultOpen = true, onOpenChange, minimized, defaultMinimized = false, onMinimizedChange,\n  boundary, defaultPosition = { x: 16, y: 16 }, onPositionChange, defaultSize, minSize = DEFAULT_MIN_SIZE, maxSize, onSizeChange,\n  resizable, closable = true, labels, className, children, ...props\n}: FloatingPanelProps) {\n  const reduceMotion = useReducedMotion() ?? false\n  const titleId = useId()\n  const hintId = useId()\n  const panelRef = useRef<HTMLDivElement>(null)\n  const merged = { ...DEFAULT_LABELS, ...labels }\n  const [isOpen, setOpen] = useControllable(open, defaultOpen, onOpenChange)\n  const [isMinimized, setMinimized] = useControllable(minimized, defaultMinimized, onMinimizedChange)\n  const docked = variant === \"docked\"\n  const geometry = usePanelGeometry({ boundary, docked, reduceMotion, defaultPosition, defaultSize: defaultSize ?? DEFAULT_SIZE[variant], minSize, maxSize }, panelRef)\n  const gestures = usePanelGestures(geometry, { docked, momentum: effect === \"momentum\", reduceMotion, onPositionChange, onSizeChange })\n  const dismiss = usePanelDismiss(setOpen, closable)\n  useFitWhenShown(isOpen, isMinimized, geometry.fit)\n  const fullHeight = docked && !isMinimized\n  const toggleMinimize = () => {\n    if (isMinimized) geometry.settleForRestore()\n    setMinimized(!isMinimized)\n  }\n  const panel = (\n    <AnimatePresence>\n      {isOpen ? (\n        <motion.div\n          ref={panelRef} role=\"dialog\" aria-labelledby={titleId} data-slot=\"floating-panel\" data-variant={variant} data-effect={effect}\n          data-side={docked ? geometry.side : undefined} data-minimized={isMinimized ? \"true\" : undefined}\n          style={{ x: geometry.x, y: geometry.y, width: variant === \"tool\" ? undefined : geometry.width }}\n          initial={reduceMotion ? false : { opacity: 0, scale: 0.98 }}\n          animate={{ opacity: 1, scale: 1 }}\n          exit={reduceMotion ? { opacity: 0, transition: INSTANT } : { opacity: 0, scale: 0.98 }}\n          transition={SPRING_DEFAULT}\n          className={panelFrameClass(variant, geometry.side, Boolean(boundary), fullHeight, className)}\n          onFocusCapture={dismiss.onFocusCapture}\n          onKeyDown={dismiss.onKeyDown}\n          {...props}\n        >\n          <span id={hintId} className=\"sr-only\">{merged.hint}</span>\n          <PanelHeader variant={variant} title={title} titleId={titleId} hintId={hintId} minimized={isMinimized} closable={closable} labels={merged} gestures={gestures} onClose={dismiss.close} onToggleMinimize={toggleMinimize} />\n          <PanelBody variant={variant} minimized={isMinimized} fullHeight={fullHeight} reduceMotion={reduceMotion} geometry={geometry}>{children}</PanelBody>\n          {(resizable ?? variant !== \"tool\") && !isMinimized ? <ResizeHandle variant={variant} title={title} hintId={hintId} labels={merged} side={geometry.side} gestures={gestures} /> : null}\n        </motion.div>\n      ) : null}\n    </AnimatePresence>\n  )\n  return boundary ? panel : <Portal.Root>{panel}</Portal.Root>\n}\n\nexport {\n  FloatingPanel,\n  type FloatingPanelProps,\n  type FloatingPanelVariant,\n  type FloatingPanelEffect,\n  type FloatingPanelLabels,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/floating-panel-gestures.ts",
      "content": "import type * as React from \"react\"\nimport { useRef } from \"react\"\nimport { projectMomentum } from \"@/registry/miseui/lib/motion\"\nimport {\n  clampPosition,\n  dockedX,\n  nearestSide,\n  type PanelPoint,\n  type PanelSize,\n} from \"@/registry/miseui/ui/floating-panel-math\"\nimport type { PanelGeometry } from \"@/registry/miseui/ui/floating-panel-state\"\n\nconst KEY_STEP_PX = 16\nconst KEY_STEP_LARGE_PX = 48\nconst ARROWS: Record<string, PanelPoint> = {\n  ArrowLeft: { x: -1, y: 0 },\n  ArrowRight: { x: 1, y: 0 },\n  ArrowUp: { x: 0, y: -1 },\n  ArrowDown: { x: 0, y: 1 },\n}\n\ninterface PanelGestureOptions {\n  docked: boolean\n  momentum: boolean\n  reduceMotion: boolean\n  onPositionChange?: (position: PanelPoint) => void\n  onSizeChange?: (size: PanelSize) => void\n}\n\ninterface GestureStart {\n  pointerX: number\n  pointerY: number\n  startX: number\n  startY: number\n  startWidth: number\n  startHeight: number\n}\n\nfunction usePanelMove(geometry: PanelGeometry, options: PanelGestureOptions, gesture: React.RefObject<GestureStart | null>) {\n  const { x, y, measured, bounds, settle, setSide } = geometry\n  const { docked, momentum, reduceMotion, onPositionChange } = options\n  const onMove = (event: React.PointerEvent<HTMLElement>) => {\n    const start = gesture.current\n    if (!start) return\n    const next = clampPosition(\n      { x: start.startX + event.clientX - start.pointerX, y: start.startY + event.clientY - start.pointerY },\n      measured(),\n      bounds(),\n    )\n    x.set(next.x)\n    y.set(next.y)\n  }\n  const onMoveEnd = () => {\n    if (!gesture.current) return\n    gesture.current = null\n    const size = measured()\n    const area = bounds()\n    const velocity = { x: x.getVelocity(), y: y.getVelocity() }\n    const here = { x: x.get(), y: y.get() }\n    if (docked) {\n      const side = nearestSide(here.x, size.width, area.width)\n      const target = { x: dockedX(side, size.width, area.width), y: 0 }\n      setSide(side)\n      settle(target, velocity)\n      onPositionChange?.(target)\n    } else if (momentum && !reduceMotion) {\n      const thrown = { x: here.x + projectMomentum(velocity.x), y: here.y + projectMomentum(velocity.y) }\n      const target = clampPosition(thrown, size, area)\n      settle(target, velocity, target.x === thrown.x && target.y === thrown.y)\n      onPositionChange?.(target)\n    } else {\n      onPositionChange?.(here)\n    }\n  }\n  const onMoveKeyDown = (event: React.KeyboardEvent) => {\n    const arrow = ARROWS[event.key]\n    if (!arrow || docked) return\n    event.preventDefault()\n    const step = event.shiftKey ? KEY_STEP_LARGE_PX : KEY_STEP_PX\n    const next = clampPosition({ x: x.get() + arrow.x * step, y: y.get() + arrow.y * step }, measured(), bounds())\n    x.set(next.x)\n    y.set(next.y)\n    onPositionChange?.(next)\n  }\n  return { onMove, onMoveEnd, onMoveKeyDown }\n}\n\nfunction usePanelResize(geometry: PanelGeometry, options: PanelGestureOptions, gesture: React.RefObject<GestureStart | null>) {\n  const { width, height, side, resizeTo } = geometry\n  const { docked, onSizeChange } = options\n  // The handle of a right dock sits on its left edge, so pulling left makes it wider.\n  const sign = docked && side === \"right\" ? -1 : 1\n  const onResize = (event: React.PointerEvent<HTMLElement>) => {\n    const start = gesture.current\n    if (!start) return\n    resizeTo({\n      width: start.startWidth + (event.clientX - start.pointerX) * sign,\n      height: docked ? start.startHeight : start.startHeight + event.clientY - start.pointerY,\n    })\n  }\n  const onResizeEnd = () => {\n    if (!gesture.current) return\n    gesture.current = null\n    onSizeChange?.({ width: width.get(), height: height.get() })\n  }\n  const onResizeKeyDown = (event: React.KeyboardEvent) => {\n    const arrow = ARROWS[event.key]\n    if (!arrow) return\n    event.preventDefault()\n    const step = event.shiftKey ? KEY_STEP_LARGE_PX : KEY_STEP_PX\n    const next = {\n      width: width.get() + arrow.x * step * sign,\n      height: docked ? height.get() : height.get() + arrow.y * step,\n    }\n    onSizeChange?.(resizeTo(next))\n  }\n  return { onResize, onResizeEnd, onResizeKeyDown }\n}\n\nfunction usePanelGestures(geometry: PanelGeometry, options: PanelGestureOptions) {\n  const { x, y, width, height } = geometry\n  const gesture = useRef<GestureStart | null>(null)\n  const begin = (event: React.PointerEvent<HTMLElement>) => {\n    // Capture keeps the moves coming when a fast pointer outruns the panel or leaves the window.\n    event.currentTarget.setPointerCapture?.(event.pointerId)\n    x.stop()\n    y.stop()\n    gesture.current = {\n      pointerX: event.clientX,\n      pointerY: event.clientY,\n      startX: x.get(),\n      startY: y.get(),\n      startWidth: width.get(),\n      startHeight: height.get(),\n    }\n  }\n  const onMoveStart = (event: React.PointerEvent<HTMLElement>) => {\n    if (event.button !== 0 || (event.target as HTMLElement).closest(\"button\")) return\n    begin(event)\n  }\n  const move = usePanelMove(geometry, options, gesture)\n  const resize = usePanelResize(geometry, options, gesture)\n  return { ...move, ...resize, begin, onMoveStart }\n}\n\ntype PanelGestures = ReturnType<typeof usePanelGestures>\n\nexport { usePanelGestures, type PanelGestures }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/floating-panel-math.ts",
      "content": "interface PanelPoint {\n  x: number\n  y: number\n}\n\ninterface PanelSize {\n  width: number\n  height: number\n}\n\ntype DockSide = \"left\" | \"right\"\n\nconst clamp = (value: number, low: number, high: number) => Math.min(Math.max(value, low), Math.max(low, high))\n\n/** Keeps the whole panel on screen. A panel larger than the bounds pins to the top left, where its title bar stays reachable. */\nfunction clampPosition(position: PanelPoint, size: PanelSize, bounds: PanelSize): PanelPoint {\n  return {\n    x: clamp(position.x, 0, bounds.width - size.width),\n    y: clamp(position.y, 0, bounds.height - size.height),\n  }\n}\n\n/** Never smaller than `min`, and never past `max` or the room left between the panel's corner and the bounds. */\nfunction clampSize(size: PanelSize, position: PanelPoint, bounds: PanelSize, min: PanelSize, max?: Partial<PanelSize>): PanelSize {\n  return {\n    width: clamp(size.width, min.width, Math.min(max?.width ?? Infinity, bounds.width - position.x)),\n    height: clamp(size.height, min.height, Math.min(max?.height ?? Infinity, bounds.height - position.y)),\n  }\n}\n\n/** The side the panel's centre is nearer to. */\nfunction nearestSide(x: number, width: number, boundsWidth: number): DockSide {\n  return x + width / 2 < boundsWidth / 2 ? \"left\" : \"right\"\n}\n\nfunction dockedX(side: DockSide, width: number, boundsWidth: number) {\n  return side === \"left\" ? 0 : Math.max(boundsWidth - width, 0)\n}\n\nexport { clamp, clampPosition, clampSize, dockedX, nearestSide, type DockSide, type PanelPoint, type PanelSize }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/floating-panel-parts.tsx",
      "content": "import { GripHorizontal, Minus, Plus, X } from \"lucide-react\"\nimport type { PanelGestures } from \"@/registry/miseui/ui/floating-panel-gestures\"\nimport type { DockSide } from \"@/registry/miseui/ui/floating-panel-math\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype FloatingPanelVariant = \"panel\" | \"compact\" | \"docked\" | \"tool\"\n\ninterface FloatingPanelLabels {\n  move: (title: string) => string\n  resize: (title: string) => string\n  minimize: string\n  restore: string\n  close: string\n  /** How to use the handles with a keyboard, read out once. */\n  hint: string\n}\n\nconst BUTTON_CLASS = cn(\n  \"flex shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none pointer-coarse:size-10\",\n  \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:bg-accent hover:text-foreground\",\n  \"focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4\",\n)\n\nconst HEADER_CLASS: Record<FloatingPanelVariant, string> = {\n  panel: \"min-h-10 gap-2 border-b px-3 py-1\",\n  compact: \"min-h-8 gap-1 border-b px-2 pointer-coarse:min-h-10\",\n  docked: \"min-h-12 gap-2 border-b px-4 py-1\",\n  tool: \"min-h-6 justify-center px-1 pointer-coarse:min-h-10\",\n}\n\ninterface PanelHeaderProps {\n  variant: FloatingPanelVariant\n  title: string\n  titleId: string\n  hintId: string\n  minimized: boolean\n  closable: boolean\n  labels: FloatingPanelLabels\n  gestures: PanelGestures\n  onToggleMinimize: () => void\n  onClose: () => void\n}\n\nfunction PanelHeader({ variant, title, titleId, hintId, minimized, closable, labels, gestures, onToggleMinimize, onClose }: PanelHeaderProps) {\n  const tool = variant === \"tool\"\n  const buttonSize = variant === \"compact\" || tool ? \"size-6\" : \"size-8\"\n  return (\n    <div data-slot=\"floating-panel-header\" className={cn(\"flex items-center\", HEADER_CLASS[variant])}>\n      <div\n        role=\"button\"\n        tabIndex={0}\n        aria-label={labels.move(title)}\n        aria-describedby={hintId}\n        aria-roledescription=\"move handle\"\n        data-slot=\"floating-panel-handle\"\n        className={cn(\n          \"flex min-h-6 min-w-0 flex-1 cursor-grab touch-none items-center self-stretch rounded-sm outline-none select-none active:cursor-grabbing\",\n          \"focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          tool && \"justify-center text-muted-foreground\",\n        )}\n        onPointerDown={gestures.onMoveStart}\n        onPointerMove={gestures.onMove}\n        onPointerUp={gestures.onMoveEnd}\n        onPointerCancel={gestures.onMoveEnd}\n        onKeyDown={gestures.onMoveKeyDown}\n      >\n        {tool ? (\n          <GripHorizontal aria-hidden=\"true\" className=\"size-4\" />\n        ) : (\n          <span id={titleId} className={cn(\"truncate font-medium\", variant === \"compact\" ? \"text-[13px]\" : \"text-sm\")}>{title}</span>\n        )}\n      </div>\n      {tool ? <span id={titleId} className=\"sr-only\">{title}</span> : null}\n      <button\n        type=\"button\"\n        aria-label={minimized ? labels.restore : labels.minimize}\n        aria-expanded={!minimized}\n        className={cn(BUTTON_CLASS, buttonSize)}\n        onClick={onToggleMinimize}\n      >\n        {minimized ? <Plus aria-hidden=\"true\" /> : <Minus aria-hidden=\"true\" />}\n      </button>\n      {closable ? (\n        <button type=\"button\" aria-label={labels.close} className={cn(BUTTON_CLASS, buttonSize)} onClick={onClose}>\n          <X aria-hidden=\"true\" />\n        </button>\n      ) : null}\n    </div>\n  )\n}\n\ninterface ResizeHandleProps {\n  variant: FloatingPanelVariant\n  title: string\n  hintId: string\n  labels: FloatingPanelLabels\n  side: DockSide\n  gestures: PanelGestures\n}\n\n/** A panel corner everywhere except the dock, which is full height and only has an inner edge to pull. */\nfunction ResizeHandle({ variant, title, hintId, labels, side, gestures }: ResizeHandleProps) {\n  const docked = variant === \"docked\"\n  const edge = side === \"right\" ? \"left-0\" : \"right-0\"\n  return (\n    <div\n      role=\"button\"\n      tabIndex={0}\n      aria-label={labels.resize(title)}\n      aria-describedby={hintId}\n      aria-roledescription=\"resize handle\"\n      data-slot=\"floating-panel-resize\"\n      className={cn(\n        \"absolute touch-none outline-none select-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\",\n        docked\n          ? cn(\"inset-y-0 w-2 cursor-ew-resize hover:bg-accent pointer-coarse:w-4\", edge)\n          : \"right-0 bottom-0 flex size-4 cursor-nwse-resize items-end justify-end rounded-br-lg p-1 pointer-coarse:size-10\",\n      )}\n      onPointerDown={gestures.begin}\n      onPointerMove={gestures.onResize}\n      onPointerUp={gestures.onResizeEnd}\n      onPointerCancel={gestures.onResizeEnd}\n      onKeyDown={gestures.onResizeKeyDown}\n    >\n      {docked ? null : <span aria-hidden=\"true\" className=\"size-2 rounded-br-sm border-r-2 border-b-2 border-muted-foreground/50\" />}\n    </div>\n  )\n}\n\n/** Each look is its own surface; `docked` loses the rounding on the side that meets the edge. */\nfunction surfaceClass(variant: FloatingPanelVariant, side: DockSide) {\n  const shared = \"border bg-popover text-popover-foreground\"\n  if (variant === \"docked\") return cn(shared, \"shadow-md\", side === \"right\" ? \"rounded-l-lg border-r-0\" : \"rounded-r-lg border-l-0\")\n  if (variant === \"compact\") return cn(shared, \"rounded-md shadow-md\")\n  if (variant === \"tool\") return cn(shared, \"w-fit rounded-xl shadow-lg\")\n  return cn(shared, \"rounded-lg shadow-lg\")\n}\n\nexport { PanelHeader, ResizeHandle, surfaceClass, type FloatingPanelLabels, type FloatingPanelVariant }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/floating-panel-state.ts",
      "content": "import type * as React from \"react\"\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { animate, useMotionValue, type MotionValue } from \"motion/react\"\nimport { SPRING_DEFAULT, SPRING_MOMENTUM } from \"@/registry/miseui/lib/motion\"\nimport {\n  clampPosition,\n  clampSize,\n  dockedX,\n  type DockSide,\n  type PanelPoint,\n  type PanelSize,\n} from \"@/registry/miseui/ui/floating-panel-math\"\n\ninterface PanelGeometryOptions {\n  boundary?: React.RefObject<HTMLElement | null>\n  docked: boolean\n  reduceMotion: boolean\n  defaultPosition: PanelPoint\n  defaultSize: PanelSize\n  minSize: PanelSize\n  maxSize?: Partial<PanelSize>\n}\n\nfunction readBounds(boundary?: React.RefObject<HTMLElement | null>): PanelSize {\n  const element = boundary?.current\n  return element ? { width: element.clientWidth, height: element.clientHeight } : { width: window.innerWidth, height: window.innerHeight }\n}\n\nfunction placeAt(x: MotionValue<number>, y: MotionValue<number>, target: PanelPoint) {\n  x.set(target.x)\n  y.set(target.y)\n}\n\n/** Overshoot only in open space: past an edge it would break the promise of staying inside. */\nfunction springTo(x: MotionValue<number>, y: MotionValue<number>, target: PanelPoint, velocity: PanelPoint, free: boolean) {\n  const spring = free ? SPRING_MOMENTUM : SPRING_DEFAULT\n  animate(x, target.x, { ...spring, velocity: velocity.x })\n  animate(y, target.y, { ...spring, velocity: velocity.y })\n}\n\n/** Re-fits when the window or the boundary changes size, so a panel is never stranded off screen. */\nfunction useKeepInside(boundary: React.RefObject<HTMLElement | null> | undefined, fit: () => void) {\n  useEffect(() => {\n    fit()\n    window.addEventListener(\"resize\", fit)\n    const element = boundary?.current\n    const observer = element && typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(fit) : null\n    if (element) observer?.observe(element)\n    return () => {\n      window.removeEventListener(\"resize\", fit)\n      observer?.disconnect()\n    }\n  }, [boundary, fit])\n}\n\n/** Where the panel is and how large, and the rules that keep it on screen. */\nfunction usePanelGeometry(options: PanelGeometryOptions, panelRef: React.RefObject<HTMLDivElement | null>) {\n  const { boundary, docked, reduceMotion, defaultPosition, defaultSize, minSize, maxSize } = options\n  // Motion values, so a drag moves one to one under the pointer with no React render per frame.\n  const x = useMotionValue(defaultPosition.x)\n  const y = useMotionValue(defaultPosition.y)\n  const width = useMotionValue(defaultSize.width)\n  const height = useMotionValue(defaultSize.height)\n  const [side, setSide] = useState<DockSide>(\"right\")\n  const bounds = useCallback(() => readBounds(boundary), [boundary])\n  // The real box where it has one, because `tool` is as wide as its contents and a minimised panel is shorter than `height`.\n  const measured = useCallback((): PanelSize => {\n    const element = panelRef.current\n    return { width: element?.offsetWidth || width.get(), height: element?.offsetHeight || height.get() }\n  }, [panelRef, width, height])\n  const settle = (target: PanelPoint, velocity?: PanelPoint, free = false) => {\n    if (reduceMotion || !velocity) placeAt(x, y, target)\n    else springTo(x, y, target, velocity, free)\n  }\n  const fit = useCallback(() => {\n    const area = bounds()\n    if (docked) {\n      x.set(dockedX(side, measured().width, area.width))\n      y.set(0)\n      return\n    }\n    placeAt(x, y, clampPosition({ x: x.get(), y: y.get() }, measured(), area))\n  }, [bounds, docked, measured, side, x, y])\n  useKeepInside(boundary, fit)\n  const resizeTo = (next: PanelSize) => {\n    const area = bounds()\n    // The right dock grows towards the left and takes its x along.\n    const position = docked ? { x: 0, y: 0 } : { x: x.get(), y: y.get() }\n    const size = clampSize(next, position, area, minSize, maxSize)\n    width.set(size.width)\n    height.set(size.height)\n    if (docked) x.set(dockedX(side, size.width, area.width))\n    return size\n  }\n  /** Runs before the body opens, so the panel moves up in step with it and never hangs off the bottom edge. */\n  const settleForRestore = () => {\n    const box = measured()\n    const full = { width: box.width, height: box.height + height.get() }\n    settle(clampPosition({ x: x.get(), y: y.get() }, full, bounds()), { x: 0, y: 0 })\n  }\n  return { x, y, width, height, side, setSide, bounds, measured, settle, fit, resizeTo, settleForRestore }\n}\n\nfunction useControllable(value: boolean | undefined, initial: boolean, onChange?: (next: boolean) => void) {\n  const [uncontrolled, setUncontrolled] = useState(initial)\n  const set = (next: boolean) => {\n    setUncontrolled(next)\n    onChange?.(next)\n  }\n  return [value ?? uncontrolled, set] as const\n}\n\n/** Remembers where focus came from so closing can put it back, and closes on Escape. */\nfunction usePanelDismiss(setOpen: (open: boolean) => void, closable: boolean) {\n  const returnFocus = useRef<HTMLElement | null>(null)\n  const close = () => {\n    setOpen(false)\n    if (returnFocus.current?.isConnected) returnFocus.current.focus()\n  }\n  const onFocusCapture = (event: React.FocusEvent<HTMLElement>) => {\n    const from = event.relatedTarget as HTMLElement | null\n    if (from && !event.currentTarget.contains(from)) returnFocus.current = from\n  }\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    if (event.key === \"Escape\" && closable) close()\n  }\n  return { close, onFocusCapture, onKeyDown }\n}\n\n/** Reopened on a window that has since shrunk, it must not come back off screen. */\nfunction useFitWhenShown(open: boolean, minimized: boolean, fit: () => void) {\n  useEffect(() => {\n    if (open) fit()\n  }, [open, minimized, fit])\n}\n\ntype PanelGeometry = ReturnType<typeof usePanelGeometry>\n\nexport { useControllable, useFitWhenShown, usePanelDismiss, usePanelGeometry, type PanelGeometry, type PanelGeometryOptions }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "overlays"
  ],
  "type": "registry:ui"
}