{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable",
  "title": "Resizable",
  "description": "Two panes with a draggable divider, horizontal or vertical, with limits that stretch, a keyboard-operable separator and a double-click reset.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/resizable.tsx",
      "content": "import type * as React from \"react\"\nimport { useId } from \"react\"\nimport { motion, type MotionStyle, type MotionValue } from \"motion/react\"\nimport { ResizableHandle, type ResizableVariant } from \"@/registry/miseui/ui/resizable-handle\"\nimport { FULL } from \"@/registry/miseui/ui/resizable-math\"\nimport { useResizeControl, type ResizableDirection } from \"@/registry/miseui/ui/resizable-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype ResizableEffect = \"snap\"\n\nconst DEFAULT_SIZE = 50\nconst DEFAULT_MIN = 10\nconst DEFAULT_MAX = 90\nconst DEFAULT_STEP = 5\n\n/** Space the handle takes in the row, in px; the first pane's share is worked out from what remains. */\nconst HANDLE_PX: Record<ResizableVariant, number> = { line: 1, grip: 12, gap: 8, hidden: 0 }\n\nconst ROOT_CLASS: Record<ResizableVariant, string> = {\n  line: \"overflow-hidden rounded-lg border\",\n  grip: \"overflow-hidden rounded-lg border\",\n  gap: \"\",\n  hidden: \"overflow-hidden rounded-lg\",\n}\n\nconst PANEL_CLASS: Record<ResizableVariant, string> = {\n  line: \"\",\n  grip: \"\",\n  gap: \"rounded-lg border bg-card data-[collapsed=true]:border-0\",\n  hidden: \"\",\n}\n\ntype ResizableProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Exactly two panes. Nest another Resizable inside one for a third. */\n  children: readonly [React.ReactNode, React.ReactNode]\n  /** `line` a hairline, `grip` a bar with a visible handle, `gap` two cards apart, `hidden` nothing until hovered. */\n  variant?: ResizableVariant\n  /** `snap` lets a flick collapse a pane, judged by where the momentum would carry it. */\n  effect?: ResizableEffect\n  direction?: ResizableDirection\n  /** Share of the first pane, in percent. Hands the size to you. */\n  size?: number\n  /** Also what a double-click on the handle goes back to. */\n  defaultSize?: number\n  min?: number\n  max?: number\n  /** Percent moved by one arrow key. */\n  step?: number\n  disabled?: boolean\n  /** Name of the handle for assistive tech. */\n  handleLabel?: string\n  onSizeChange?: (size: number) => void\n}\n\nfunction firstPaneStyle(live: MotionValue<number>, handlePx: number) {\n  // clamp: the momentum spring may overshoot a collapse, and a pane cannot be narrower than nothing.\n  return {\n    \"--resizable-size\": live,\n    flexBasis: `calc((100% - ${handlePx}px) * clamp(0, var(--resizable-size), ${FULL}) / ${FULL})`,\n  } as MotionStyle\n}\n\ninterface ResizablePaneProps {\n  variant: ResizableVariant\n  collapsed: boolean\n  id?: string\n  /** Only the first pane has a size of its own; the second takes what is left. */\n  style?: MotionStyle\n  children: React.ReactNode\n}\n\nfunction ResizablePane({ variant, collapsed, id, style, children }: ResizablePaneProps) {\n  return (\n    <motion.div\n      id={id}\n      data-slot=\"resizable-panel\"\n      data-collapsed={collapsed}\n      inert={collapsed}\n      className={cn(\"min-h-0 min-w-0 overflow-auto\", style ? \"shrink-0 grow-0\" : \"flex-1\", PANEL_CLASS[variant])}\n      style={style}\n    >\n      {children}\n    </motion.div>\n  )\n}\n\nfunction Resizable({\n  children,\n  variant = \"line\",\n  effect,\n  direction = \"horizontal\",\n  size,\n  defaultSize = DEFAULT_SIZE,\n  min = DEFAULT_MIN,\n  max = DEFAULT_MAX,\n  step = DEFAULT_STEP,\n  disabled = false,\n  handleLabel = \"Resize panels\",\n  onSizeChange,\n  className,\n  ...props\n}: ResizableProps) {\n  const firstId = useId()\n  const collapsible = effect === \"snap\"\n  const handlePx = HANDLE_PX[variant]\n  const options = { direction, size, defaultSize, limits: { min, max }, step, collapsible, disabled, handlePx, onSizeChange }\n  const { current, live, dragging, handlers } = useResizeControl(options)\n  const horizontal = direction === \"horizontal\"\n  return (\n    <div\n      data-slot=\"resizable\"\n      data-variant={variant}\n      data-effect={effect}\n      data-direction={direction}\n      data-dragging={dragging || undefined}\n      className={cn(\"flex min-h-0 min-w-0\", horizontal ? \"flex-row\" : \"flex-col\", dragging && \"select-none\", ROOT_CLASS[variant], className)}\n      {...props}\n    >\n      <ResizablePane id={firstId} variant={variant} collapsed={current <= 0} style={firstPaneStyle(live, handlePx)}>\n        {children[0]}\n      </ResizablePane>\n      <ResizableHandle\n        {...{ variant, horizontal, dragging, disabled }}\n        {...{ label: handleLabel, controls: firstId, value: current }}\n        // A pane that can collapse really does reach 0 and 100, whatever the drag limits are.\n        min={collapsible ? 0 : min}\n        max={collapsible ? FULL : max}\n        {...handlers}\n      />\n      <ResizablePane variant={variant} collapsed={current >= FULL}>\n        {children[1]}\n      </ResizablePane>\n    </div>\n  )\n}\n\nexport { Resizable, type ResizableProps, type ResizableVariant, type ResizableEffect, type ResizableDirection }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/resizable-state.ts",
      "content": "import type * as React from \"react\"\nimport { useEffect, useRef, useState } from \"react\"\nimport { animate, useMotionValue, useReducedMotion, type Transition } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { axisSign, useResizeDrag, type DragControls } from \"@/registry/miseui/ui/resizable-drag\"\nimport { FULL, sizeForKey, type SizeLimits } from \"@/registry/miseui/ui/resizable-math\"\n\ntype ResizableDirection = \"horizontal\" | \"vertical\"\n\ninterface ResizeOptions {\n  direction: ResizableDirection\n  size: number | undefined\n  defaultSize: number\n  limits: SizeLimits\n  step: number\n  collapsible: boolean\n  disabled: boolean\n  handlePx: number\n  onSizeChange?: (size: number) => void\n}\n\nfunction useSizeState({ size, defaultSize, onSizeChange }: ResizeOptions) {\n  const reduceMotion = useReducedMotion() ?? false\n  const [uncontrolled, setUncontrolled] = useState(defaultSize)\n  const current = size ?? uncontrolled\n  // Painted size. Separate from `current` because it runs past the limits mid-drag and between frames of a spring.\n  const live = useMotionValue(current)\n  const requested = useRef(current)\n  const animation = useRef<ReturnType<typeof animate> | null>(null)\n  const lastOpen = useRef(defaultSize)\n\n  const commit = (next: number) => {\n    requested.current = next\n    if (next === current) return\n    setUncontrolled(next)\n    onSizeChange?.(next)\n  }\n  const stop = () => animation.current?.stop()\n  const jump = (next: number) => {\n    stop()\n    live.set(next)\n    commit(next)\n  }\n  const settle = (next: number, transition: Transition = SPRING_DEFAULT) => {\n    commit(next)\n    stop()\n    if (reduceMotion) live.set(next)\n    else animation.current = animate(live, next, transition)\n  }\n  /** What Enter restores. Taken before a pane collapses, because a flick has already dragged the size away by then. */\n  const rememberOpen = (next: number) => {\n    if (next > 0 && next < FULL) lastOpen.current = next\n  }\n  const openSize = () => lastOpen.current\n\n  // No dependency list on purpose: a parent that ignores onSizeChange changes nothing, and has to win all the same.\n  useEffect(() => {\n    if (requested.current === current) return\n    requested.current = current\n    animation.current?.stop()\n    live.set(current)\n  })\n  useEffect(() => () => animation.current?.stop(), [])\n\n  return { current, live, reduceMotion, commit, stop, jump, settle, rememberOpen, openSize }\n}\n\nfunction useResizeControl(options: ResizeOptions) {\n  const { direction, defaultSize, limits, step, collapsible, disabled } = options\n  const state = useSizeState(options)\n  const { current, live, jump, settle, rememberOpen, openSize } = state\n  const controls: DragControls = state\n  const { dragging, handlers } = useResizeDrag(options, controls)\n  const collapsed = current <= 0 || current >= FULL\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n    if (disabled) return\n    const sign = axisSign(event.currentTarget, direction)\n    const next = sizeForKey({ key: event.key, horizontal: direction === \"horizontal\", sign, current, step, limits })\n    if (next !== null) {\n      event.preventDefault()\n      jump(next)\n      return\n    }\n    if (event.key !== \"Enter\") return\n    event.preventDefault()\n    if (!collapsible) {\n      settle(defaultSize)\n    } else if (collapsed) {\n      settle(openSize())\n    } else {\n      rememberOpen(current)\n      settle(0)\n    }\n  }\n\n  const onDoubleClick = () => {\n    if (!disabled) settle(defaultSize)\n  }\n\n  return { current, live, dragging, handlers: { ...handlers, onKeyDown, onDoubleClick } }\n}\n\nexport { useResizeControl, type ResizableDirection }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/resizable-drag.ts",
      "content": "import type * as React from \"react\"\nimport { useRef, useState } from \"react\"\nimport type { MotionValue, Transition } from \"motion/react\"\nimport { SPRING_MOMENTUM } from \"@/registry/miseui/lib/motion\"\nimport {\n  FULL,\n  addSample,\n  clampSize,\n  dragBounds,\n  flickTarget,\n  restingSize,\n  stretchSize,\n  velocityOf,\n  type DragSample,\n  type SizeLimits,\n} from \"@/registry/miseui/ui/resizable-math\"\n\ntype DragDirection = \"horizontal\" | \"vertical\"\ntype HandlePointerEvent = React.PointerEvent<HTMLElement>\n\ninterface DragOptions {\n  direction: DragDirection\n  limits: SizeLimits\n  collapsible: boolean\n  disabled: boolean\n  handlePx: number\n}\n\ninterface DragControls {\n  live: MotionValue<number>\n  reduceMotion: boolean\n  commit: (size: number) => void\n  stop: () => void\n  settle: (size: number, transition?: Transition) => void\n  rememberOpen: (size: number) => void\n}\n\ninterface DragSession {\n  pointerId: number\n  startPosition: number\n  startSize: number\n  sign: number\n  lengthPx: number\n  samples: readonly DragSample[]\n}\n\n/** Arrows and pointer travel run the other way in a right-to-left row, where the first pane sits on the right. */\nfunction axisSign(element: Element, direction: DragDirection) {\n  return direction === \"horizontal\" && getComputedStyle(element).direction === \"rtl\" ? -1 : 1\n}\n\nfunction positionOf(event: HandlePointerEvent, direction: DragDirection) {\n  return direction === \"horizontal\" ? event.clientX : event.clientY\n}\n\nfunction createSession(event: HandlePointerEvent, root: Element, startSize: number, options: DragOptions): DragSession {\n  const { direction, handlePx } = options\n  const rect = root.getBoundingClientRect()\n  const position = positionOf(event, direction)\n  return {\n    pointerId: event.pointerId,\n    startPosition: position,\n    startSize,\n    sign: axisSign(root, direction),\n    lengthPx: (direction === \"horizontal\" ? rect.width : rect.height) - handlePx,\n    samples: [{ position, time: performance.now() }],\n  }\n}\n\nfunction rawSize(session: DragSession, position: number) {\n  const travelled = ((position - session.startPosition) * session.sign) / Math.max(1, session.lengthPx)\n  return session.startSize + travelled * FULL\n}\n\n/** Where a release goes. The spring starts at the speed of the hand, so there is no seam between the drag and the settle. */\nfunction releaseTarget(session: DragSession, shown: number, flickable: boolean, limits: SizeLimits) {\n  const velocityPx = velocityOf(session.samples, performance.now()) * session.sign\n  const flick = flickable ? flickTarget(shown, velocityPx, session.lengthPx, limits) : null\n  if (flick === null) return { size: restingSize(shown, session.startSize, limits) }\n  const velocity = (velocityPx / Math.max(1, session.lengthPx)) * FULL\n  return { size: flick, transition: { ...SPRING_MOMENTUM, velocity } }\n}\n\nfunction useResizeDrag(options: DragOptions, controls: DragControls) {\n  const { direction, limits, collapsible, disabled } = options\n  const { live, reduceMotion, commit, stop, settle, rememberOpen } = controls\n  const session = useRef<DragSession | null>(null)\n  const [dragging, setDragging] = useState(false)\n\n  const onPointerDown = (event: HandlePointerEvent) => {\n    const root = event.currentTarget.parentElement\n    if (disabled || event.button !== 0 || !root) return\n    // Capture keeps the drag alive when the pointer outruns the thin handle or leaves the window.\n    event.currentTarget.setPointerCapture(event.pointerId)\n    stop()\n    rememberOpen(live.get())\n    session.current = createSession(event, root, live.get(), options)\n    setDragging(true)\n  }\n\n  const onPointerMove = (event: HandlePointerEvent) => {\n    const active = session.current\n    if (!active || event.pointerId !== active.pointerId) return\n    const position = positionOf(event, direction)\n    const raw = rawSize(active, position)\n    const bounds = dragBounds(active.startSize, limits)\n    live.set(reduceMotion ? clampSize(raw, bounds) : stretchSize(raw, bounds, active.lengthPx))\n    session.current = { ...active, samples: addSample(active.samples, { position, time: performance.now() }) }\n    commit(clampSize(raw, bounds))\n  }\n\n  const finish = (event: HandlePointerEvent, allowFlick: boolean) => {\n    const active = session.current\n    if (!active || event.pointerId !== active.pointerId) return\n    session.current = null\n    setDragging(false)\n    const { size, transition } = releaseTarget(active, live.get(), allowFlick && collapsible, limits)\n    settle(size, transition)\n  }\n\n  return {\n    dragging,\n    handlers: {\n      onPointerDown,\n      onPointerMove,\n      onPointerUp: (event: HandlePointerEvent) => finish(event, true),\n      onPointerCancel: (event: HandlePointerEvent) => finish(event, false),\n      onLostPointerCapture: (event: HandlePointerEvent) => finish(event, false),\n    },\n  }\n}\n\nexport { axisSign, useResizeDrag, type DragControls }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/resizable-handle.tsx",
      "content": "import type * as React from \"react\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype ResizableVariant = \"line\" | \"grip\" | \"gap\" | \"hidden\"\ntype HandleAxis = \"x\" | \"y\"\n\nconst HANDLE_AXIS_CLASS: Record<HandleAxis, string> = {\n  // The visible part is often 1 px wide; the pseudo-element is what the hand actually aims at.\n  x: \"cursor-col-resize after:inset-y-0 after:-inset-x-2 pointer-coarse:after:-inset-x-5\",\n  y: \"cursor-row-resize after:inset-x-0 after:-inset-y-2 pointer-coarse:after:-inset-y-5\",\n}\n\nconst HANDLE_CLASS: Record<ResizableVariant, Record<HandleAxis, string>> = {\n  line: { x: \"w-px bg-border\", y: \"h-px bg-border\" },\n  grip: { x: \"w-3 border-x bg-muted\", y: \"h-3 border-y bg-muted\" },\n  gap: { x: \"w-2\", y: \"h-2\" },\n  hidden: { x: \"w-0\", y: \"h-0\" },\n}\n\nconst LINE_ACTIVE_CLASS = \"hover:bg-foreground/30 focus-visible:bg-mise data-[dragging=true]:bg-mise\"\n\nconst MARK_CLASS: Record<Exclude<ResizableVariant, \"line\">, Record<HandleAxis, string>> = {\n  grip: { x: \"h-8 w-1 rounded-full bg-muted-foreground/40\", y: \"h-1 w-8 rounded-full bg-muted-foreground/40\" },\n  gap: { x: \"h-8 w-1 rounded-full bg-muted-foreground/60\", y: \"h-1 w-8 rounded-full bg-muted-foreground/60\" },\n  hidden: { x: \"absolute inset-y-0 w-0.5 bg-mise\", y: \"absolute inset-x-0 h-0.5 bg-mise\" },\n}\n\n/** `grip` is always on show; `gap` and `hidden` keep their mark out of sight until someone reaches for the handle. */\nconst MARK_REVEAL_CLASS: Record<Exclude<ResizableVariant, \"line\">, string> = {\n  grip: \"group-hover/handle:bg-muted-foreground group-data-[dragging=true]/handle:bg-mise\",\n  gap: \"opacity-0 group-hover/handle:opacity-100 group-focus-visible/handle:opacity-100 group-data-[dragging=true]/handle:bg-mise group-data-[dragging=true]/handle:opacity-100\",\n  hidden: \"opacity-0 group-hover/handle:opacity-100 group-focus-visible/handle:opacity-100 group-data-[dragging=true]/handle:opacity-100\",\n}\n\nfunction HandleMark({ variant, axis }: { variant: ResizableVariant; axis: HandleAxis }) {\n  if (variant === \"line\") return null\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-part=\"mark\"\n      className={cn(\n        \"transition-[opacity,background-color] duration-(--duration-fast) ease-(--ease-mise)\",\n        MARK_CLASS[variant][axis],\n        MARK_REVEAL_CLASS[variant],\n      )}\n    />\n  )\n}\n\ntype ResizableHandleProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  variant: ResizableVariant\n  horizontal: boolean\n  label: string\n  /** The pane this handle resizes. */\n  controls: string\n  value: number\n  min: number\n  max: number\n  dragging: boolean\n  disabled: boolean\n}\n\nfunction ResizableHandle({\n  variant,\n  horizontal,\n  label,\n  controls,\n  value,\n  min,\n  max,\n  dragging,\n  disabled,\n  ...handlers\n}: ResizableHandleProps) {\n  const axis: HandleAxis = horizontal ? \"x\" : \"y\"\n  return (\n    <div\n      role=\"separator\"\n      tabIndex={disabled ? undefined : 0}\n      aria-label={label}\n      aria-orientation={horizontal ? \"vertical\" : \"horizontal\"}\n      aria-controls={controls}\n      aria-valuenow={Math.round(value)}\n      aria-valuemin={min}\n      aria-valuemax={max}\n      aria-disabled={disabled || undefined}\n      data-slot=\"resizable-handle\"\n      data-variant={variant}\n      data-dragging={dragging}\n      className={cn(\n        \"group/handle relative z-10 flex shrink-0 touch-none items-center justify-center outline-none select-none\",\n        \"transition-colors duration-(--duration-fast) ease-(--ease-mise)\",\n        \"after:absolute focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n        HANDLE_AXIS_CLASS[axis],\n        HANDLE_CLASS[variant][axis],\n        variant === \"line\" && LINE_ACTIVE_CLASS,\n        disabled && \"pointer-events-none opacity-50\",\n      )}\n      {...handlers}\n    >\n      <HandleMark variant={variant} axis={axis} />\n    </div>\n  )\n}\n\nexport { ResizableHandle, type ResizableVariant }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/resizable-math.ts",
      "content": "import { projectMomentum, rubberband } from \"@/registry/miseui/lib/motion\"\n\nconst FULL = 100\n/** Only the last moments of a drag say how fast the hand was going when it let go. */\nconst SAMPLE_WINDOW_MS = 100\n/** Slower than this is a careful placement, not a flick, however close the edge is. */\nconst FLICK_MIN_SPEED_PX = 500\n\ninterface SizeLimits {\n  min: number\n  max: number\n}\n\ninterface DragSample {\n  position: number\n  time: number\n}\n\nfunction clampSize(size: number, { min, max }: SizeLimits) {\n  return Math.min(max, Math.max(min, size))\n}\n\n/** The size to paint during a drag: exact between the limits, then resisting more the further past them. */\nfunction stretchSize(raw: number, limits: SizeLimits, lengthPx: number) {\n  const clamped = clampSize(raw, limits)\n  if (clamped === raw || lengthPx <= 0) return clamped\n  const overshootPx = ((raw - clamped) / FULL) * lengthPx\n  return clamped + (rubberband(overshootPx, lengthPx) / lengthPx) * FULL\n}\n\nfunction addSample(samples: readonly DragSample[], sample: DragSample) {\n  return [...samples.filter((entry) => sample.time - entry.time <= SAMPLE_WINDOW_MS), sample]\n}\n\n/** Pixels per second at release. A hand that stopped and then let go has no velocity, whatever came before. */\nfunction velocityOf(samples: readonly DragSample[], now: number) {\n  const recent = samples.filter((entry) => now - entry.time <= SAMPLE_WINDOW_MS)\n  if (recent.length < 2) return 0\n  const first = recent[0]\n  const last = recent[recent.length - 1]\n  const elapsed = last.time - first.time\n  return elapsed > 0 ? ((last.position - first.position) / elapsed) * 1000 : 0\n}\n\n/** Where a flick lands: fully collapsed one way or the other, or null when it would stop inside the limits. */\nfunction flickTarget(size: number, velocityPx: number, lengthPx: number, limits: SizeLimits) {\n  if (Math.abs(velocityPx) < FLICK_MIN_SPEED_PX || lengthPx <= 0) return null\n  const projected = size + (projectMomentum(velocityPx) / lengthPx) * FULL\n  if (projected < limits.min) return 0\n  if (projected > limits.max) return FULL\n  return null\n}\n\n/** A drag that starts collapsed may travel through the closed zone without jumping straight to the limit. */\nfunction dragBounds(startSize: number, limits: SizeLimits): SizeLimits {\n  return { min: Math.min(limits.min, startSize), max: Math.max(limits.max, startSize) }\n}\n\n/** Where a released drag settles. A collapsed pane pulled less than halfway to its limit stays collapsed. */\nfunction restingSize(shown: number, startSize: number, limits: SizeLimits) {\n  if (startSize < limits.min && shown < limits.min / 2) return startSize\n  if (startSize > limits.max && shown > (limits.max + FULL) / 2) return startSize\n  return clampSize(shown, limits)\n}\n\ninterface KeyContext {\n  key: string\n  horizontal: boolean\n  /** -1 when arrows run against the size, as in a right-to-left layout. */\n  sign: number\n  current: number\n  step: number\n  limits: SizeLimits\n}\n\n/** Size after an arrow, Home or End; null for any other key. */\nfunction sizeForKey({ key, horizontal, sign, current, step, limits }: KeyContext) {\n  const grow = horizontal ? \"ArrowRight\" : \"ArrowDown\"\n  const shrink = horizontal ? \"ArrowLeft\" : \"ArrowUp\"\n  if (key === grow) return clampSize(current + step * sign, limits)\n  if (key === shrink) return clampSize(current - step * sign, limits)\n  if (key === \"Home\") return limits.min\n  if (key === \"End\") return limits.max\n  return null\n}\n\nexport {\n  FULL,\n  addSample,\n  clampSize,\n  dragBounds,\n  flickTarget,\n  restingSize,\n  sizeForKey,\n  stretchSize,\n  velocityOf,\n  type DragSample,\n  type SizeLimits,\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}