Components
Drag Items
Free-form draggable positioning within a bounded container. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add drag-itemspnpm dlx kosmesis add drag-itemsyarn dlx kosmesis add drag-itemsbunx kosmesis add drag-itemsCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, Prop, Ref, State, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";export interface DragItemsProps { class?: string; id?: string; children?: Children;}@Component()export class DragItems extends StatelessComponent<DragItemsProps> { render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="drag-items" class={cn("relative size-full", cls)}> {children} </div> ); }}export interface DragItemPosition { x: number; y: number;}export interface DragItemProps { defaultPosition?: DragItemPosition; bounded?: boolean; class?: string; children?: Children;}@Component()export class DragItem extends StatefulComponent { @Prop() defaultPosition: DragItemPosition = { x: 0, y: 0 }; @Prop() bounded = true; @Prop() class?: string; @Prop() children?: Children; @Ref<HTMLDivElement>() elRef!: RefType<HTMLDivElement>; @State() _pos: DragItemPosition = { x: 0, y: 0 }; @State() _dragging = false; private _origin: DragItemPosition = { x: 0, y: 0 }; private _start: DragItemPosition = { x: 0, y: 0 }; onBeforeMount() { this._pos = { ...this.defaultPosition }; } private readonly _handlePointerDown = (event: PointerEvent) => { const el = this.elRef.current; if (!el) return; event.preventDefault(); this._dragging = true; this._origin = { ...this._pos }; this._start = { x: event.clientX, y: event.clientY }; el.setPointerCapture(event.pointerId); }; private readonly _handlePointerMove = (event: PointerEvent) => { if (!this._dragging) return; const el = this.elRef.current; const parent = el?.parentElement; let x = this._origin.x + (event.clientX - this._start.x); let y = this._origin.y + (event.clientY - this._start.y); if (this.bounded && el && parent) { const maxX = Math.max(parent.clientWidth - el.offsetWidth, 0); const maxY = Math.max(parent.clientHeight - el.offsetHeight, 0); x = Math.min(Math.max(x, 0), maxX); y = Math.min(Math.max(y, 0), maxY); } this._pos = { x, y }; }; private readonly _handlePointerUp = () => { this._dragging = false; }; render() { return ( <div ref={this.elRef} data-slot="drag-item" data-dragging={() => (this._dragging ? "" : undefined)} class={cn( "absolute cursor-grab touch-none select-none data-dragging:z-50 data-dragging:cursor-grabbing", this.class, )} style={() => ({ transform: `translate(${String(this._pos.x)}px, ${String(this._pos.y)}px)` })} onPointerDown={this._handlePointerDown} onPointerMove={this._handlePointerMove} onPointerUp={this._handlePointerUp} > {this.children} </div> ); }}Install the following dependencies:
npm install @praxisjs/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled } from "@praxisjs/css";import { Component, Prop, Ref, State, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";class DragItemsStyles extends Stylesheet { $root = this.css({ position: "relative", width: "100%", height: "100%" }); $item = this.css({ position: "absolute", cursor: "grab", touchAction: "none", userSelect: "none", }).on("&[data-dragging]", { zIndex: 50, cursor: "grabbing" });}export interface DragItemsProps { class?: string; id?: string; children?: Children;}@Component()export class DragItems extends StatelessComponent<DragItemsProps> { @Styled(DragItemsStyles) $s!: DragItemsStyles; render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="drag-items" class={cx(this.$s.$root, cls)}> {children} </div> ); }}export interface DragItemPosition { x: number; y: number;}export interface DragItemProps { defaultPosition?: DragItemPosition; bounded?: boolean; class?: string; children?: Children;}@Component()export class DragItem extends StatefulComponent { @Styled(DragItemsStyles) $s!: DragItemsStyles; @Prop() defaultPosition: DragItemPosition = { x: 0, y: 0 }; @Prop() bounded = true; @Prop() class?: string; @Prop() children?: Children; @Ref<HTMLDivElement>() elRef!: RefType<HTMLDivElement>; @State() _pos: DragItemPosition = { x: 0, y: 0 }; @State() _dragging = false; private _origin: DragItemPosition = { x: 0, y: 0 }; private _start: DragItemPosition = { x: 0, y: 0 }; onBeforeMount() { this._pos = { ...this.defaultPosition }; } private readonly _handlePointerDown = (event: PointerEvent) => { const el = this.elRef.current; if (!el) return; event.preventDefault(); this._dragging = true; this._origin = { ...this._pos }; this._start = { x: event.clientX, y: event.clientY }; el.setPointerCapture(event.pointerId); }; private readonly _handlePointerMove = (event: PointerEvent) => { if (!this._dragging) return; const el = this.elRef.current; const parent = el?.parentElement; let x = this._origin.x + (event.clientX - this._start.x); let y = this._origin.y + (event.clientY - this._start.y); if (this.bounded && el && parent) { const maxX = Math.max(parent.clientWidth - el.offsetWidth, 0); const maxY = Math.max(parent.clientHeight - el.offsetHeight, 0); x = Math.min(Math.max(x, 0), maxX); y = Math.min(Math.max(y, 0), maxY); } this._pos = { x, y }; }; private readonly _handlePointerUp = () => { this._dragging = false; }; render() { return ( <div ref={this.elRef} data-slot="drag-item" data-dragging={() => (this._dragging ? "" : undefined)} class={cx(this.$s.$item, this.class)} style={() => ({ transform: `translate(${String(this._pos.x)}px, ${String(this._pos.y)}px)` })} onPointerDown={this._handlePointerDown} onPointerMove={this._handlePointerMove} onPointerUp={this._handlePointerUp} > {this.children} </div> ); }}Examples
About
Purely presentational — no Morphos equivalent. Pointer-drag math the same shape as
ResizableHandle's, translating the element instead of resizing a neighbor. Distinct from
Sortable, which reorders a rank instead of repositioning elements freely — this is a
moodboard/canvas item, not a list.
Usage
import { DragItem, DragItems } from "@/components/ui/drag-items";
<DragItems>
<DragItem defaultPosition={{ x: 24, y: 24 }}>A</DragItem>
<DragItem defaultPosition={{ x: 140, y: 80 }}>B</DragItem>
</DragItems>Props
DragItem
| Prop | Type | Default |
|---|---|---|
defaultPosition | { x: number; y: number } | { x: 0, y: 0 } |
bounded | boolean | true (clamp within the parent DragItems) |
class | string | — |
DragItems accepts class, id, and children only.