Skip to content
drawer-floating-system logo

Drawer & Floating Panel System for TMNL

drawer-floating-system

Drawer and floating panel system for TMNL. Invoke when implementing drawer stacks, floating panels, drag/resize behaviors, or parallax animations. Provides Rolodex stack, stx-powered state, and container query patterns.

SKILL.md

Full skill instructions

Drawer & Floating Panel System for TMNL

Overview

A comprehensive panel management system with:

  • Drawer Stack — Rolodex-style stacking with parallax lift animations
  • Floating Panels — stx-powered state machines with 8-direction resize
  • Global/​Panel Slots — Composable content injection system
  • Motion Blur — Velocity-based blur effects during drag
  • Persistence — localStorage-backed panel geometry

Canonical Sources

TMNL Implementations

FilePurposePattern
src/​lib/​drawer/​Drawer.tsxMain drawer componentCompound component
src/​lib/​drawer/​DrawerStackContext.tsxStack state managementReact Context
src/​lib/​drawer/​GlobalSlot.tsxGlobal content injectionSlot pattern
src/​lib/​drawer/​PanelSlot.tsxPer-drawer content slotsSlot pattern
src/​lib/​drawer/​animations/​rolodex.tsRolodex stack animationGSAP driver
src/​lib/​drawer/​animations/​parallax-lift.tsParallax scroll effectTransform math
src/​lib/​floating/​FloatingPanel.tsxFloating panel componentstx + resize
src/​lib/​floating/​floating-stx.tsLegend-State + XState hybridstx pattern
src/​lib/​floating/​withDraggable.tsxDrag behavior HOCPointer events
src/​lib/​floating/​ResizeHandles.tsx8-direction resizeEdge detection

Testbeds

  • DrawerTestbed: /​testbed/​drawer — Rolodex stack, slot injection
  • FloatingPanelTestbed: /​testbed/​floating — Drag, resize, persistence

Pattern 1: Drawer Stack Management — ROLODEX PATTERN

When: Managing multiple stacked drawers with animated transitions.

The drawer stack uses a LIFO (Last-In-First-Out) model with visual stacking:

import { createContext, useContext, useReducer } from 'react'

interface DrawerState {
  drawers: DrawerInstance[]
  activeId: string | null
}

type DrawerAction =
  | { type: 'PUSH'; drawer: DrawerInstance }
  | { type: 'POP' }
  | { type: 'POP_TO'; id: string }
  | { type: 'REPLACE'; drawer: DrawerInstance }

const drawerReducer = (state: DrawerState, action: DrawerAction): DrawerState => {
  switch (action.type) {
    case 'PUSH':
      return {
        drawers: [...state.drawers, action.drawer],
        activeId: action.drawer.id,
      }
    case 'POP':
      const popped = state.drawers.slice(0, -1)
      return {
        drawers: popped,
        activeId: popped[popped.length - 1]?.id ?? null,
      }
    case 'POP_TO': {
      const idx = state.drawers.findIndex(d => d.id === action.id)
      if (idx === -1) return state
      const remaining = state.drawers.slice(0, idx + 1)
      return {
        drawers: remaining,
        activeId: action.id,
      }
    }
    case 'REPLACE': {
      const replaced = [...state.drawers.slice(0, -1), action.drawer]
      return {
        drawers: replaced,
        activeId: action.drawer.id,
      }
    }
  }
}

const DrawerStackContext = createContext<{
  state: DrawerState
  dispatch: React.Dispatch<DrawerAction>
} | null>(null)

TMNL Location: src/​lib/​drawer/​DrawerStackContext.tsx


Pattern 2: Parallax Stack Animation — DEPTH EFFECTS

When: Creating visual depth with offset and scale transformations.

import { gsap } from 'gsap'

interface ParallaxConfig {
  offsetY: number      // Vertical offset per layer (px)
  scaleDecay: number   // Scale reduction per layer (0-1)
  opacityDecay: number // Opacity reduction per layer (0-1)
}

const DEFAULT_CONFIG: ParallaxConfig = {
  offsetY: 20,
  scaleDecay: 0.02,
  opacityDecay: 0.15,
}

function applyParallaxStack(
  drawers: HTMLElement[],
  config: ParallaxConfig = DEFAULT_CONFIG
) {
  const active = drawers.length - 1

  drawers.forEach((el, idx) => {
    const depth = active - idx  // 0 = active, 1+ = behind

    gsap.to(el, {
      y: -depth * config.offsetY,
      scale: 1 - depth * config.scaleDecay,
      opacity: 1 - depth * config.opacityDecay,
      duration: 0.3,
      ease: 'power2.out',
    })
  })
}

Key Formula: transform: translateY(${-depth * offsetY}px) scale(${1 - depth * scaleDecay})

TMNL Location: src/​lib/​drawer/​animations/​parallax-lift.ts


Pattern 3: TableService Integration — DATA-AWARE DRAWERS

When: Connecting drawers to data sources via table-service.

import { TableService } from '@/​lib/​table-service'

interface DataDrawerProps<T> {
  tableService: TableService<T>
  rowId: string
  children: (data: T) => React.ReactNode
}

function DataDrawer<T>({ tableService, rowId, children }: DataDrawerProps<T>) {
  const row = useTableRow(tableService, rowId)

  if (!row) return null

  return (
    <Drawer id={`data-${rowId}`} title={`Row ${rowId}`}>
      {children(row)}
    </​Drawer>
  )
}

// Usage
<DataDrawer tableService={assetService} rowId="asset-123">
  {(asset) => (
    <div>
      <h2>{asset.name}</​h2>
      <AssetDetails asset={asset} />
    </​div>
  )}
</​DataDrawer>

TMNL Location: Integration pattern documented in src/​lib/​table-service/


Pattern 4: stx-Powered Floating Panels — HYBRID STATE

When: Managing complex panel state with Legend-State reactivity + XState machines.

The "stx" pattern combines:

  • Legend-State — Fine-grained reactive state (position, size)
  • XState — Lifecycle state machine (idle, dragging, resizing)
import { observable } from '@legendapp/​state'
import { setup, createActor } from 'xstate'

// Legend-State for reactive data
const panelState = observable({
  panels: new Map<string, PanelState>(),
  zOrder: [] as string[],
})

// XState for lifecycle
const panelMachine = setup({
  types: {
    context: {} as { panelId: string; initialPos: Position },
    events: {} as
      | { type: 'DRAG_START'; x: number; y: number }
      | { type: 'DRAG_MOVE'; x: number; y: number }
      | { type: 'DRAG_END' }
      | { type: 'RESIZE_START'; edge: ResizeEdge }
      | { type: 'RESIZE_MOVE'; x: number; y: number }
      | { type: 'RESIZE_END' },
  },
}).createMachine({
  id: 'panel',
  initial: 'idle',
  states: {
    idle: {
      on: {
        DRAG_START: 'dragging',
        RESIZE_START: 'resizing',
      },
    },
    dragging: {
      on: {
        DRAG_MOVE: { actions: 'updatePosition' },
        DRAG_END: 'idle',
      },
    },
    resizing: {
      on: {
        RESIZE_MOVE: { actions: 'updateSize' },
        RESIZE_END: 'idle',
      },
    },
  },
})

// Combine into stx
const getFloatingStx = () => ({
  data: panelState,
  machine: panelMachine,
  computed: {
    topPanel: () => panelState.zOrder.get()[panelState.zOrder.get().length - 1],
    motionBlurStyle: (velocity: number) => ({
      filter: `blur(${Math.min(velocity * 0.1, 4)}px)`,
    }),
  },
  effects: {
    persist: () => {
      localStorage.setItem('panels', JSON.stringify(panelState.get()))
    },
    restore: () => {
      const saved = localStorage.getItem('panels')
      if (saved) panelState.set(JSON.parse(saved))
    },
  },
})

TMNL Location: src/​lib/​floating/​floating-stx.ts


Pattern 5: FloatingPanel Component — 8-DIRECTION RESIZE

When: Creating resizable, draggable floating panels.

import { useFloatingPanel } from './​hooks/​useFloatingPanel'
import { ResizeHandles } from './​ResizeHandles'

interface FloatingPanelProps {
  id: string
  title: string
  initialPosition?: Position
  initialSize?: Size
  minSize?: Size
  maxSize?: Size
  children: React.ReactNode
}

function FloatingPanel({
  id,
  title,
  initialPosition = { x: 100, y: 100 },
  initialSize = { width: 320, height: 240 },
  minSize = { width: 200, height: 150 },
  maxSize,
  children,
}: FloatingPanelProps) {
  const {
    position,
    size,
    zIndex,
    isDragging,
    isResizing,
    handleDragStart,
    handleResizeStart,
    bringToFront,
  } = useFloatingPanel(id, { initialPosition, initialSize })

  return (
    <div
      className="floating-panel"
      style={{
        position: 'fixed',
        left: position.x,
        top: position.y,
        width: size.width,
        height: size.height,
        zIndex,
      }}
      onPointerDown={bringToFront}
    >
      {/​* Title bar (drag handle) */​}
      <div
        className="panel-titlebar"
        onPointerDown={handleDragStart}
      >
        {title}
      </​div>

      {/​* Content */​}
      <div className="panel-content">
        {children}
      </​div>

      {/​* Resize handles */​}
      <ResizeHandles
        onResizeStart={handleResizeStart}
        minSize={minSize}
        maxSize={maxSize}
      />
    </​div>
  )
}

TMNL Location: src/​lib/​floating/​FloatingPanel.tsx


Pattern 6: withDraggable HOC — POINTER EVENT MANAGEMENT

When: Adding drag behavior to any component.

import { useCallback, useRef, useState } from 'react'

interface DraggableOptions {
  onDragStart?: (pos: Position) => void
  onDrag?: (pos: Position, delta: Position) => void
  onDragEnd?: (pos: Position) => void
  constraint?: 'none' | 'horizontal' | 'vertical' | DOMRect
}

function withDraggable<P extends object>(
  Component: React.ComponentType<P>,
  options: DraggableOptions = {}
) {
  return function DraggableComponent(props: P) {
    const [isDragging, setIsDragging] = useState(false)
    const startPos = useRef<Position>({ x: 0, y: 0 })
    const currentPos = useRef<Position>({ x: 0, y: 0 })

    const handlePointerDown = useCallback((e: React.PointerEvent) => {
      e.preventDefault()
      e.currentTarget.setPointerCapture(e.pointerId)

      startPos.current = { x: e.clientX, y: e.clientY }
      currentPos.current = { x: e.clientX, y: e.clientY }
      setIsDragging(true)

      options.onDragStart?.(startPos.current)
    }, [])

    const handlePointerMove = useCallback((e: React.PointerEvent) => {
      if (!isDragging) return

      const delta = {
        x: e.clientX - currentPos.current.x,
        y: e.clientY - currentPos.current.y,
      }
      currentPos.current = { x: e.clientX, y: e.clientY }

      options.onDrag?.(currentPos.current, delta)
    }, [isDragging])

    const handlePointerUp = useCallback((e: React.PointerEvent) => {
      e.currentTarget.releasePointerCapture(e.pointerId)
      setIsDragging(false)

      options.onDragEnd?.(currentPos.current)
    }, [])

    return (
      <Component
        {...props}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        data-dragging={isDragging}
      />
    )
  }
}

TMNL Location: src/​lib/​floating/​withDraggable.tsx


Pattern 7: Container Queries — RESPONSIVE PANELS

When: Adapting panel content to available space.

// Use container queries for panel-intrinsic sizing
function ResponsivePanelContent({ children }: { children: React.ReactNode }) {
  return (
    <div className="@container panel-content">
      <div className="@[200px]:grid-cols-1 @[400px]:grid-cols-2 @[600px]:grid-cols-3 grid gap-4">
        {children}
      </​div>
    </​div>
  )
}

// CSS (Tailwind container queries)
// @container panel-content (min-width: 400px) { ... }

Alternative: FloatingDimensionContext

import { createContext, useContext } from 'react'

const FloatingDimensionContext = createContext<Size>({ width: 320, height: 240 })

function FloatingPanel({ size, children }) {
  return (
    <FloatingDimensionContext.Provider value={size}>
      {children}
    </​FloatingDimensionContext.Provider>
  )
}

function PanelContent() {
  const { width, height } = useContext(FloatingDimensionContext)

  // Adapt to available dimensions
  const columns = width < 300 ? 1 : width < 500 ? 2 : 3
  return <Grid columns={columns}>...</​Grid>
}

TMNL Location: src/​lib/​floating/​FloatingDimensionContext.tsx


Pattern 8: Motion Blur — VELOCITY-BASED EFFECTS

When: Adding motion blur during fast drag operations.

const calculateMotionBlur = (velocity: number): string => {
  // velocity = pixels/​frame
  const blurAmount = Math.min(velocity * 0.15, 8)  // Cap at 8px
  const direction = velocity > 0 ? 'horizontal' : 'vertical'

  if (blurAmount < 0.5) return 'none'

  return `blur(${blurAmount}px)`
}

// In drag handler
const handleDrag = (e: PointerEvent) => {
  const velocity = Math.sqrt(
    Math.pow(e.movementX, 2) + Math.pow(e.movementY, 2)
  )

  panelRef.current.style.filter = calculateMotionBlur(velocity)
}

const handleDragEnd = () => {
  // Animate blur removal
  gsap.to(panelRef.current, {
    filter: 'blur(0px)',
    duration: 0.2,
  })
}

TMNL Location: src/​lib/​floating/​floating-stx.ts (computed)


Pattern 9: Panel Persistence — LOCALSTORAGE GEOMETRY

When: Remembering panel positions/​sizes across sessions.

import { useEffect } from 'react'

const STORAGE_KEY = 'tmnl:panel-geometry'

interface PersistedPanel {
  id: string
  position: Position
  size: Size
  zIndex: number
}

const usePanelPersistence = (panelId: string) => {
  const loadGeometry = (): PersistedPanel | null => {
    const stored = localStorage.getItem(STORAGE_KEY)
    if (!stored) return null

    const panels: Record<string, PersistedPanel> = JSON.parse(stored)
    return panels[panelId] ?? null
  }

  const saveGeometry = (geometry: Omit<PersistedPanel, 'id'>) => {
    const stored = localStorage.getItem(STORAGE_KEY)
    const panels: Record<string, PersistedPanel> = stored ? JSON.parse(stored) : {}

    panels[panelId] = { id: panelId, ...geometry }
    localStorage.setItem(STORAGE_KEY, JSON.stringify(panels))
  }

  // Debounced save during drag/​resize
  const debouncedSave = useMemo(
    () => debounce(saveGeometry, 200),
    [panelId]
  )

  return { loadGeometry, saveGeometry: debouncedSave }
}

TMNL Location: src/​lib/​floating/​hooks/​usePanelPersistence.ts


Pattern 10: Global vs Panel Slots — CONTENT INJECTION

When: Injecting content at different levels of the drawer hierarchy.

import { createContext, useContext } from 'react'

// Global slot: content appears in all drawers
const GlobalSlotContext = createContext<React.ReactNode>(null)

function GlobalSlot({ children }: { children: React.ReactNode }) {
  return (
    <GlobalSlotContext.Provider value={children}>
      {children}
    </​GlobalSlotContext.Provider>
  )
}

// Panel slot: content scoped to specific drawer
const PanelSlotContext = createContext<Map<string, React.ReactNode>>(new Map())

function PanelSlot({
  name,
  children,
}: {
  name: string
  children: React.ReactNode
}) {
  const slots = useContext(PanelSlotContext)
  slots.set(name, children)
  return null
}

// Usage in drawer
function Drawer({ id, children }) {
  const globalContent = useContext(GlobalSlotContext)
  const [slots] = useState(() => new Map())

  return (
    <PanelSlotContext.Provider value={slots}>
      <div className="drawer">
        {/​* Global slot renders in header */​}
        <div className="drawer-header">{globalContent}</​div>

        {/​* Main content */​}
        <div className="drawer-content">{children}</​div>

        {/​* Named slots render in footer */​}
        <div className="drawer-footer">{slots.get('footer')}</​div>
      </​div>
    </​PanelSlotContext.Provider>
  )
}

// Consumer
<GlobalSlot>
  <Breadcrumbs />
</​GlobalSlot>

<Drawer id="details">
  <AssetDetails />
  <PanelSlot name="footer">
    <ActionButtons />
  </​PanelSlot>
</​Drawer>

TMNL Location: src/​lib/​drawer/​GlobalSlot.tsx, src/​lib/​drawer/​PanelSlot.tsx


Decision Tree: Component Selection

What are you building?
│
├─ Stacked content panels?
│  ├─ Sequential navigation? → Drawer Stack (Pattern 1)
│  └─ Depth effect? → Parallax Stack (Pattern 2)
│
├─ Floating window?
│  ├─ Resizable + draggable? → FloatingPanel (Pattern 5)
│  └─ Just draggable? → withDraggable HOC (Pattern 6)
│
├─ Complex state with animations?
│  └─ stx-Powered Panels (Pattern 4)
│
├─ Remember panel positions?
│  └─ Panel Persistence (Pattern 9)
│
└─ Content injection?
   ├─ All drawers? → GlobalSlot
   └─ Specific drawer? → PanelSlot

Anti-Patterns

Don't: Use transform-origin incorrectly for stacking

// BANNED - Scale from wrong origin causes visual jump
style={{ transform: `scale(${1 - depth * 0.02})` }}

// CORRECT - Scale from top center for card-like effect
style={{
  transformOrigin: 'top center',
  transform: `scale(${1 - depth * 0.02})`,
}}

Don't: Store panel position in useState during drag

// BANNED - Too many re-renders during drag
const [position, setPosition] = useState({ x: 0, y: 0 })
onDrag={(pos) => setPosition(pos)}  // 60+ re-renders/​sec

// CORRECT - Use refs for drag, sync state on end
const posRef = useRef({ x: 0, y: 0 })
onDrag={(pos) => { posRef.current = pos }}
onDragEnd={() => setPosition(posRef.current)}

Don't: Forget pointer capture

// BANNED - Drag breaks if pointer leaves element
onPointerDown={() => setDragging(true)}

// CORRECT - Capture pointer for reliable tracking
onPointerDown={(e) => {
  e.currentTarget.setPointerCapture(e.pointerId)
  setDragging(true)
}}

Integration Points

Depends on:

  • xstate-integration — Panel lifecycle machines
  • tmnl-animation-tokens — Animation timings
  • react-hoc-patterns — withDraggable HOC

Used by:

  • ag-grid-patterns — Detail panels for grid rows
  • tmnl-registry-patterns — Overlay system

Quick Reference

TaskPatternFile
Stack drawers with animationDrawer Stackdrawer/​DrawerStackContext.tsx
Create parallax depthParallax Stackdrawer/​animations/​parallax-lift.ts
Build floating panelFloatingPanelfloating/​FloatingPanel.tsx
Add drag behaviorwithDraggablefloating/​withDraggable.tsx
Manage stx statefloating-stxfloating/​floating-stx.ts
Add resize handlesResizeHandlesfloating/​ResizeHandles.tsx
Persist panel geometryusePanelPersistencefloating/​hooks/​usePanelPersistence.ts
Inject global contentGlobalSlotdrawer/​GlobalSlot.tsx

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View