npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tft/interact

v21.1.4

Published

An Angular directive library wrapping [interactjs](https://interactjs.io/) for drag-and-drop, resize, gesture, auto-scroll, and lasso selection.

Readme

@tft/interact

An Angular directive library wrapping interactjs for drag-and-drop, resize, gesture, auto-scroll, and lasso selection.


Installation

npm install @tft/interact interactjs

All directives are standalone — import them individually or use InteractModule for NgModule-based apps.

Standalone (recommended):

import { DraggableDirective, DropzoneDirective, ResizableDirective } from '@tft/interact';

@Component({
  imports: [DraggableDirective, DropzoneDirective, ResizableDirective],
})
export class MyComponent {}

NgModule (legacy):

import { InteractModule } from '@tft/interact';

@NgModule({
  imports: [InteractModule.forRoot({ cssDimensionUnit: 'px' })],
})
export class AppModule {}

InteractModule is deprecated and will be removed in the next major version. Prefer importing directives directly.


Directives

| Directive | Selector | Purpose | |---|---|---| | DraggableDirective | [tftDraggable] | Makes an element draggable | | DropzoneDirective | [tftDropzone] | Defines a drop target | | ResizableDirective | [tftResizable] | Makes an element resizable | | GesturableDirective | [tftGesturable] | Pinch/rotate gesture support | | DragRootDirective | [tftDragRoot] | Sets where the drag clone is appended | | AccountForScaleDirective | [tftAccountForScale] | Compensates coordinates for CSS scale() | | ApplyScaleDirective | [tftApplyScale] | Applies a scale value as font-size | | AutoScrollDirective | [tftAutoScroll] | Auto-scrolls a container during drag | | LassoDirective | [tftLasso] | Drag-to-select rectangle on a canvas | | DragPreviewDirective | ng-template[tftDragPreview] | Custom template for the drag clone | | ArrayOfNPipe | arrayOfN | Creates an array of N items for @for |


Draggable

<div tftDraggable
  [x]="item.x" [y]="item.y"
  [dragData]="item"
  [dragDisabled]="isLocked"
  (dragEnd)="onDragEnd($event)">
</div>

Key inputs:

| Input | Type | Default | Description | |---|---|---|---| | x | number | — | Initial x position | | y | number | — | Initial y position | | dragData | D | — | Payload attached to drag events | | dragDisabled | boolean | false | Disables drag | | dragZIndex | number | 10000 | z-index while dragging | | dragConfig | DraggableOptions | — | Raw interactjs config (inertia, modifiers, etc.) | | showPlaceholder | boolean | false | Keep original element visible while dragging | | dragRoot | DragRootDirective | — | Override drag root element | | enableDragDefault | boolean | true | Use built-in clone+position behaviour |

Outputs: dragStart, dragMove, dragInertiaStart, dragEnd — all emit TftDragEvent.

Inertia + restrict example:

dragConfig: DraggableOptions = {
  inertia: { resistance: 2, allowResume: true, endSpeed: 10, smoothEndDuration: 2000 },
  modifiers: [
    interact.modifiers.restrict({
      restriction: '.board',
      endOnly: false,
      elementRect: { top: 0, left: 0, bottom: 1, right: 1 },
    }),
  ],
};

Dropzone

<div tftDropzone
  [dropzoneId]="'yard'"
  [dropzoneConfig]="{ overlap: 0.5 }"
  (dragDrop)="onDrop($event)"
  (dragEnter)="onEnter($event)"
  (dragLeave)="onLeave($event)">
</div>

Key inputs: dropzoneId, dropzoneConfig (DropzoneOptions), dropzoneData, position (CSS position, default 'relative').

Outputs: dragDrop, dragEnter, dragLeave, dropActivate — all emit TftDropEvent.

onDrop(event: TftDropEvent) {
  if (event.dropTarget?.dropzoneId() === 'yard') { ... }
  const data = event.dragRef.dragData();
}

Resizable

<div tftResizable
  [width]="w" [height]="h"
  [resizeConfig]="{ edges: { right: true, bottom: true } }"
  (resizeEnd)="onResizeEnd($event)">
</div>

Key inputs: width, height, resizeConfig (ResizableOptions), resizeDisabled, position, enableResizeDefault.

Outputs: resizeStart, resizeMove, resizeInertiaStart, resizeEnd — all emit TftResizeEvent.

TftResizeEvent includes size: { width, height } and positionInDropTarget.


Scale Support

Use tftAccountForScale on the drag root when CSS scale() is applied:

<div tftDragRoot tftAccountForScale [scale]="scale">
  <div tftDraggable tftResizable [width]="w" [height]="h"></div>
</div>

For cross-dropzone dragging where the draggable is not a child of the drag root:

<div tftDragRoot tftAccountForScale [scale]="scale" #root="tftDragRoot">
  <div tftDropzone>
    <div tftDraggable [dragRoot]="root"></div>
  </div>
  <div tftDropzone (dragDrop)="onDrop($event)"></div>
</div>

Gesture (pinch-to-zoom)

<div tftDragRoot tftGesturable tftAccountForScale
  [scale]="scale"
  [style.transform]="'scale(' + scale + ')'"
  [style.transform-origin]="transformOrigin"
  (gestureMove)="onGestureMove($event)">
</div>
onGestureMove(event: TftGestureEvent) {
  this.scale += event.interactEvent?.ds || 0;  // ds = delta scale
  const { x0, y0 } = event.interactEvent;
  this.transformOrigin = `${x0}px ${y0}px`;
}

Auto-Scroll

<div class="scroll-container" tftAutoScroll>
  <div tftDragRoot>
    <div tftDraggable></div>
  </div>
</div>

Configure scroll sensitivity via [autoScrollConfig]:

autoScrollConfig = { margin: 50, distance: 3, interval: 50, speed: 300, enabled: true };

Drag Preview Template

<div tftDraggable [dragData]="item">
  <ng-template tftDragPreview>
    <div class="drag-ghost">{{ item.name }}</div>
  </ng-template>
  Original content
</div>

Lasso (Drag-to-Select)

tftLasso tracks a pointer drag on a background canvas and emits the selection rectangle. Hit-testing against child elements is the consumer's responsibility.

<div tftLasso #lasso="tftLasso" [scale]="scale"
     (lassoStart)="clearSelection()"
     (lassoCommit)="onLassoCommit($event)">

  @if (lasso.lassoDisplayRect(); as r) {
    <div class="lasso-rect"
         [style.left.px]="r.x" [style.top.px]="r.y"
         [style.width.px]="r.w" [style.height.px]="r.h">
    </div>
  }
</div>
onLassoCommit(rect: LassoRect) {
  // rect is in viewport (clientX/Y) space
  this.selected = this.items.filter(item => {
    const b = this.itemEl(item).getBoundingClientRect();
    return b.left < rect.right && b.right > rect.left &&
           b.top < rect.bottom && b.bottom > rect.top;
  });
}

Inputs: scale (default 1), lassoDisabled (default false). Outputs: lassoStart (void), lassoCommit (LassoRect). Exported signals: isLassoing, lassoDisplayRect (null when not dragging).

Uses Pointer Events — works with mouse, touch, and stylus.


Event Types

TftDragEvent {
  interactEvent: NgDragEvent;
  dragRef: DraggableDirective;      // dragRef.dragData() to get payload
  dragOrigin?: DropzoneDirective;
  dropTarget?: DropzoneDirective;
  positionInDropTarget: { x, y } | null;
}

TftDropEvent   // same shape as TftDragEvent

TftResizeEvent {
  interactEvent: NgResizeEvent;
  resizeRef: ResizableDirective;
  dragRef?: DraggableDirective;
  dragOrigin?: DropzoneDirective;
  dropTarget?: DropzoneDirective;
  positionInDropTarget: { x, y } | null;
  size: { width: number; height: number };
}

TftGestureEvent {
  gestureRef: GesturableDirective;
  interactEvent: GestureEvent;      // .ds = delta scale, .x0/.y0 = touch origin
}

LassoRect {
  left: number; right: number;
  top: number;  bottom: number;     // all in viewport (clientX/Y) space
}

Migration to v21.1.0

Version 21.1.0 converted all directives to standalone and replaced @Input()/@Output() with signal-based input()/output().

Import directives directly

// BEFORE
@NgModule({ imports: [InteractModule.forRoot({ cssDimensionUnit: 'px' })] })

// AFTER
@Component({ imports: [DraggableDirective, DropzoneDirective, ResizableDirective, ...] })

InteractModule still works but is deprecated. InteractService is providedIn: 'root'.

dragData and dropzoneData are now signals

// BEFORE
const team = event.dragRef.dragData.team;
const points = event.dropTarget.dropzoneData.points;

// AFTER
const team = event.dragRef.dragData().team;
const points = event.dropTarget.dropzoneData().points;

dropzoneId is now a signal

// BEFORE
if (event.dropTarget?.dropzoneId === 'yard') { ... }

// AFTER
if (event.dropTarget?.dropzoneId() === 'yard') { ... }

autoScrollConfig and scale are now signals

// BEFORE
const config = this.autoScrollDir.autoScrollConfig;
const scale = this.accountForScaleDir.scale;

// AFTER
const config = this.autoScrollDir.autoScrollConfig();
const scale = this.accountForScaleDir.scale();