KQ-2114 · SPRING/FOLDERS

Spring Folders

A folder sidebar that works as a drop target: dragging documents from the page lifts a ghost that follows the pointer 1:1, and holding it over a closed folder starts a ring that closes over springDelay (a linear timer you can read) before it flashes and the folder springs open on springs.snap, its subfolders unfolding on springs.glide so you keep descending without letting go. Dropping files the documents — the ghost shrinks into the folder, its count rolls and bumps on springs.recoil, the cards fold out of the page on glide, and folders sprung open on the way that are not on the path fold shut — while folders that refuse a type tint with a ban glyph and send the ghost home. Every card has Move to (M), which opens the same tree in a dialog where arrows walk and open folders and Enter files, and under 300px the tree is a sheet that rises when a drag begins.

KQ-2114 · SPRING/FOLDERS

Space picks a document, M moves it to a folder. Arrow keys move between documents.

Unfiled5

Drag to file
Suppliers
Millrace Steel
Signed
Templates
Archive 2024

5 unfiled · drag a card onto a folder, or press m

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/spring-folders

Props

ref
React.Ref<SpringFoldersHandle>
default —
hint(docId?, folderId?) plays the gesture once without a hand: a document glides over a folder, its ring closes, it springs open, and everything goes back.
folders / defaultFolders
SpringFolder[]
default defaultSpringFolders
The tree, controlled or initial: { id, name, count, children?, accepts?: ("pdf" | "doc" | "sheet" | "image")[] | "none" }.
docs / defaultDocs
SpringDoc[]
default defaultSpringDocs
The unfiled documents on the page, controlled or initial: { id, name, type, size (bytes) }.
onDrop
(docIds: string[], folderId: string) => void
default —
Documents were filed into a folder, by a drop or Move to.
onOpen
(folderId: string, via: "spring" | "click" | "key") => void
default —
A folder opened: sprung by a held drag, pressed, or by a key.
onCreateFolder
(parentId: string, folder: SpringFolder) => void
default —
New folder was pressed; uncontrolled, the folder is added under the selected one.
defaultExpanded
string[]
default ["suppliers"]
Folders open at the start.
springDelay
number
default 700
How long a held drag takes to spring a folder open, in ms; the ring's speed.
ringStyle
"circle" | "outline" | "fill"
default "circle"
The spring-loading ring: round the folder icon, an outline drawn round the whole row, or the row filling left to right.
accent
"cobalt" | "plum" | "mint" | "ink" | string
default "cobalt"
Ring, selection and drop targets. A name or any CSS colour.
status
"ready" | "loading" | "error"
default "ready"
Whether the tree has arrived: still skeleton rows while loading, a message and Retry on error; nothing can be dragged until it is ready.
onRetry
() => void
default —
Retry was pressed after an error.
title
string
default "Basinworks contracts"
The tree's heading and accessible name.
pageTitle
string
default "Unfiled"
The page's heading.
frameHeight
number
default 520
The frame's height in px.
disabled
boolean
default false
Read only: nothing can be moved or created.
className
string
default —
Classes for the frame.