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.
Space picks a document, M moves it to a folder. Arrow keys move between documents.
Unfiled5
Drag to file5 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| ref | React.Ref<SpringFoldersHandle> | — | 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[] | defaultSpringFolders | The tree, controlled or initial: { id, name, count, children?, accepts?: ("pdf" | "doc" | "sheet" | "image")[] | "none" }. |
| docs / defaultDocs | SpringDoc[] | defaultSpringDocs | The unfiled documents on the page, controlled or initial: { id, name, type, size (bytes) }. |
| onDrop | (docIds: string[], folderId: string) => void | — | Documents were filed into a folder, by a drop or Move to. |
| onOpen | (folderId: string, via: "spring" | "click" | "key") => void | — | A folder opened: sprung by a held drag, pressed, or by a key. |
| onCreateFolder | (parentId: string, folder: SpringFolder) => void | — | New folder was pressed; uncontrolled, the folder is added under the selected one. |
| defaultExpanded | string[] | ["suppliers"] | Folders open at the start. |
| springDelay | number | 700 | How long a held drag takes to spring a folder open, in ms; the ring's speed. |
| ringStyle | "circle" | "outline" | "fill" | "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 | "cobalt" | Ring, selection and drop targets. A name or any CSS colour. |
| status | "ready" | "loading" | "error" | "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 | — | Retry was pressed after an error. |
| title | string | "Basinworks contracts" | The tree's heading and accessible name. |
| pageTitle | string | "Unfiled" | The page's heading. |
| frameHeight | number | 520 | The frame's height in px. |
| disabled | boolean | false | Read only: nothing can be moved or created. |
| className | string | — | Classes for the frame. |