Skip to the piece

lab/Shapeshift

Try “dinner with priya friday 8pm”/

Shapeshift

An input that becomes what you mean, made to take your own components. Give each one the words it answers to and a reader for its data, then type a sentence: it works out which one you meant, opens it over the field and fills it in. It's the idea behind my Shapeshift and the bar at the foot of anishfn.ink, as a component you can install, with eight shapes to start from.

Usesmotion
Add it with shadcn
$bunx --bun shadcn@latest add https://lab.anishfn.ink/r/shapeshift.json

Give it your own components

A shape is a component of yours and two things about it: the words it answers to, and a reader that pulls its data out of a sentence. Shapeshift does the rest. Here it is with a tip card.

1. Your component. Any component with props. This one takes a percent and a bill.

components/tip-card.tsx
/** A tip, worked out from a bill. It knows nothing about Shapeshift: two props in, a card out. */
export function TipCard({ percent, bill }: { percent: number; bill: number }) {
  const tip = (bill * percent) / 100;
  return (
    <div className="flex items-end justify-between gap-4">
      <div>
        <p className="text-[12px] text-muted-foreground">
          {percent}% of {bill.toFixed(2)}
        </p>
        <p className="mt-1.5 text-[30px] leading-none font-medium tabular-nums">{tip.toFixed(2)}</p>
      </div>
      <div className="text-right">
        <p className="text-[12px] text-muted-foreground">Total</p>
        <p className="mt-1 text-[15px] font-medium tabular-nums">{(bill + tip).toFixed(2)}</p>
      </div>
    </div>
  );
}

15% of 80.00

12.00

Total

92.00

<TipCard percent={15} bill={80} />

2. Make it a shape. Say what it answers to and how to read its props from a sentence. It's live on the right: try gratuity 20% on 64.50.

components/dock.tsx
"use client";

import { Shapeshift, shape } from "@/components/shapeshift";
import { shapes } from "@/components/shapeshift-shapes";
import { TipCard } from "@/components/tip-card";

export const tip = shape({
  id: "tip",
  label: "Tip",
  hint: "A tip and the total, from a bill.",
  example: "tip 15% on 80",
  action: "Add tip",
  // 1. the words it answers to
  match: ["tip", "gratuity"],
  // 2. the sentence, read into your data. null until it's all there
  read: (text) => {
    const [percent, bill] = text.match(/\d+(\.\d+)?/g)?.map(Number) ?? [];
    return bill ? { percent, bill } : null;
  },
  // 3. your component, filled in
  render: ({ data }) => <TipCard percent={data.percent} bill={data.bill} />,
});

/** A dock at the foot of the page: the tip, then the starter shapes. */
export function Dock() {
  return (
    <div className="pointer-events-none fixed inset-x-0 bottom-6 flex justify-center px-3">
      <Shapeshift shapes={[tip, ...shapes]} onSubmit={({ shape, data }) => console.log(shape?.id, data)} />
    </div>
  );
}

Change the numbers, or press Enter to see what onSubmit gets.

TTip

15% of 80.00

12.00

Total

92.00

Tip

What it does for you

Decides calmly
A shape opens once its words are in the sentence and its reader has found the data. Mid-word it holds its last reading instead of closing. When two shapes fit, the one that reads more of the sentence wins, then the one you listed first.
Lists and keys
Focus the field, or press / anywhere, and every shape is listed, narrowing as you type. Arrows move, Enter picks one or hands its data to onSubmit, Esc clears.
One button
Give a shape an action and the field grows a button for it, the same as Enter. If your component is a form, that button submits the form.
Your theme
Written in shadcn's own colors (popover, muted, primary, border, ring), so it takes your palette and your dark mode. Every part takes a class of yours too.
A model, if you like
Pass classify and a model picks the shape from the sentence. Your readers still fill in the data, so it decides and the code computes.
Where it opens
Upward by default, for a bar at the foot of a page. side="bottom" opens it downward, and restWidth lets it rest narrow and widen when it's used.
Comes from
Shapeshift, my input that becomes what you mean, and the bar at the foot of anishfn.ink, which is this with my own panels in it.
Made with
React, Tailwind and Motion. No model and no other library: matching and reading are plain code you can read.
PreviousAnish's wallNextAxolotl vs Heron

© 2026anishfn

made with care