---
title: Note find
description: "Finds notes by what they are: a pitch, ranges of velocity, length and chance, in or out of the scale, active or not, every few, or the rest."
component: true
base: base
---

```tsx
"use client";

import {
  findNotes,
  type Note,
  type NoteId,
  type NoteQuery,
} from "@audio-ui/core/piano-roll";
import type { Scale } from "@audio-ui/core/pitch";
import { useCallback, useState } from "react";
import { NoteFind } from "@/components/audio-ui/note-find";
import {
  PianoRoll,
  PianoRollGrid,
  PianoRollKeys,
  PianoRollStrip,
  PianoRollViewport,
} from "@/components/audio-ui/piano-roll";
import { Timeline } from "@/components/audio-ui/timeline";

const SCALE: Scale = { name: "major", root: 0 };

const PHRASE: Note[] = [
  { id: "1", length: 2, pitch: 60, start: 0, velocity: 0.9 },
  { id: "2", length: 2, pitch: 64, start: 2, velocity: 0.5 },
  { id: "3", length: 2, pitch: 67, start: 4, velocity: 0.8 },
  { id: "4", length: 2, pitch: 70, start: 6, velocity: 1 },
  { id: "5", length: 1, pitch: 71, start: 8, velocity: 0.4 },
  { id: "6", length: 1, pitch: 67, start: 9, velocity: 0.7 },
  { id: "7", length: 2, pitch: 66, start: 10, velocity: 0.6 },
  { id: "8", length: 4, pitch: 62, start: 12, muted: true, velocity: 0.8 },
];

/** Find notes by what they are: the ones found are selected on the roll. */
export function NoteFindDemo() {
  const [notes, setNotes] = useState(PHRASE);
  const [selection, setSelection] = useState<readonly NoteId[]>([]);
  const selectFound = useCallback(
    (query: NoteQuery) => setSelection(findNotes(notes, query)),
    [notes]
  );

  return (
    <div className="flex w-full flex-col gap-3">
      <div className="flex items-center justify-between gap-2">
        <p aria-live="polite" className="text-muted-foreground text-sm">
          {selection.length} of {notes.length} notes selected
        </p>
        <NoteFind onFind={selectFound} scale={SCALE} />
      </div>
      <Timeline duration={16} snapStep={1}>
        <PianoRoll
          className="h-64 rounded-md border"
          maxPitch={72}
          minPitch={60}
          onSelectionChange={setSelection}
          onValueChange={setNotes}
          scale={SCALE}
          selection={selection}
          value={notes}
        >
          <PianoRollViewport>
            <PianoRollStrip className="flex-1" lead={<PianoRollKeys />}>
              <PianoRollGrid accentStep={4} aria-label="Notes" gridStep={1} />
            </PianoRollStrip>
          </PianoRollViewport>
        </PianoRoll>
      </Timeline>
    </div>
  );
}
```

## Installation

<CodeTabs>

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash component
npx shadcn@latest add @audio-ui/note-find
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the following dependencies:</Step>

```bash
npm install @audio-ui/core
```

<Step>Copy and paste the following code into your project.</Step>

```tsx
"use client";

import type { NoteQuery } from "@audio-ui/core/piano-roll";
import { pitchClassName, type Scale } from "@audio-ui/core/pitch";
import {
  type ChangeEvent,
  type ComponentProps,
  useCallback,
  useId,
  useState,
} from "react";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

/** The form's fields, as typed: empty means no limit. */
const NO_FIND = {
  chanceFrom: "",
  chanceTo: "",
  every: "1",
  lengthFrom: "",
  lengthTo: "",
  pitch: "any",
  rest: false,
  scale: "any",
  state: "any",
  velocityFrom: "",
  velocityTo: "",
};
type FindFields = typeof NO_FIND;

/** A from…to pair read off two fields, divided by `unit` into the note's terms, or nothing when both are empty. */
function rangeOf(
  from: string,
  to: string,
  unit: number
): [number, number] | undefined {
  if (from === "" && to === "") {
    return;
  }
  return [
    from === "" ? 0 : Number(from) / unit,
    to === "" ? Number.POSITIVE_INFINITY : Number(to) / unit,
  ];
}

/** The query the fields ask for. */
function findQuery(fields: FindFields, scale: Scale | null): NoteQuery {
  return {
    chance: rangeOf(fields.chanceFrom, fields.chanceTo, 100),
    every: { count: Number(fields.every), offset: 0 },
    invert: fields.rest,
    length: rangeOf(fields.lengthFrom, fields.lengthTo, 1),
    muted: fields.state === "any" ? undefined : fields.state === "muted",
    pitchClasses: fields.pitch === "any" ? undefined : [Number(fields.pitch)],
    scale:
      scale && fields.scale !== "any"
        ? { scale, within: fields.scale === "in" }
        : undefined,
    velocity: rangeOf(fields.velocityFrom, fields.velocityTo, 127),
  };
}

const FIND_ROW = "grid grid-cols-[4.5rem_1fr_1fr] items-center gap-1.5";

/** A from…to pair of number fields, `name`From and `name`To, empty for no limit. */
function RangeField({
  fields,
  label,
  max,
  min,
  name,
  onChange,
}: {
  fields: FindFields;
  label: string;
  max: number;
  min: number;
  name: "chance" | "length" | "velocity";
  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}) {
  return (
    <fieldset className={FIND_ROW}>
      <legend className="contents text-muted-foreground text-xs">
        {label}
      </legend>
      {(["From", "To"] as const).map((end) => (
        <Input
          aria-label={`${label} ${end === "From" ? "from" : "to"}`}
          className="h-7 tabular-nums"
          inputMode="numeric"
          key={end}
          max={max}
          min={min}
          name={`${name}${end}`}
          onChange={onChange}
          placeholder={String(end === "From" ? min : max)}
          type="number"
          value={fields[`${name}${end}`]}
        />
      ))}
    </fieldset>
  );
}

interface FindItem {
  label: string;
  value: string;
}
const PITCH_ITEMS: FindItem[] = [
  { label: "Any", value: "any" },
  ...Array.from({ length: 12 }, (_, root) => ({
    label: pitchClassName(root),
    value: String(root),
  })),
];
const SCALE_ITEMS: FindItem[] = [
  { label: "Any", value: "any" },
  { label: "In the scale", value: "in" },
  { label: "Out of it", value: "out" },
];
const STATE_ITEMS: FindItem[] = [
  { label: "Any", value: "any" },
  { label: "Active", value: "active" },
  { label: "Deactivated", value: "muted" },
];
const EVERY_ITEMS: FindItem[] = [
  { label: "Note", value: "1" },
  { label: "Second note", value: "2" },
  { label: "Third note", value: "3" },
  { label: "Fourth note", value: "4" },
];

/** One labelled choice of the form. */
function FindSelect({
  disabled = false,
  items,
  label,
  name,
  onField,
  value,
}: {
  disabled?: boolean;
  items: FindItem[];
  label: string;
  name: keyof FindFields;
  onField: (name: string, value: string) => void;
  value: string;
}) {
  const id = useId();
  const handleChange = useCallback(
    (next: string | null) => onField(name, next ?? items[0]?.value ?? ""),
    [items, name, onField]
  );
  return (
    <div className="grid grid-cols-[4.5rem_1fr] items-center gap-1.5">
      <span className="text-muted-foreground text-xs" id={id}>
        {label}
      </span>
      <Select
        disabled={disabled}
        items={items}
        onValueChange={handleChange}
        value={value}
      >
        <SelectTrigger aria-labelledby={id} className="w-full" size="sm">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          <SelectGroup>
            {items.map((item) => (
              <SelectItem key={item.value} value={item.value}>
                {item.label}
              </SelectItem>
            ))}
          </SelectGroup>
        </SelectContent>
      </Select>
    </div>
  );
}

interface NoteFindFormProps
  extends Omit<ComponentProps<"div">, "children" | "onChange"> {
  /** Fired with the query when Select is pressed: pass it to `findNotes` from `@audio-ui/core/piano-roll`. */
  onFind: (query: NoteQuery) => void;
  /** The scale marked, which the "Scale" choice reads; unset, the choice is off. */
  scale: Scale | null;
}

/** The fields that find notes by what they are: a pitch, ranges of velocity, length and chance, in or out of the scale, active or not, every few, or the rest. */
function NoteFindForm({
  className,
  onFind,
  scale,
  ...props
}: NoteFindFormProps) {
  const id = useId();
  const [fields, setFields] = useState<FindFields>(NO_FIND);
  const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
    const { checked, name, type, value } = event.currentTarget;
    setFields((current) => ({
      ...current,
      [name]: type === "checkbox" ? checked : value,
    }));
  }, []);
  const find = useCallback(
    () => onFind(findQuery(fields, scale)),
    [fields, onFind, scale]
  );
  const onField = useCallback(
    (name: string, value: string) =>
      setFields((current) => ({ ...current, [name]: value })),
    []
  );
  return (
    <div
      className={cn("flex flex-col gap-2", className)}
      data-slot="note-find-form"
      {...props}
    >
      <FindSelect
        items={PITCH_ITEMS}
        label="Pitch"
        name="pitch"
        onField={onField}
        value={fields.pitch}
      />
      <FindSelect
        disabled={!scale}
        items={SCALE_ITEMS}
        label="Scale"
        name="scale"
        onField={onField}
        value={fields.scale}
      />
      <FindSelect
        items={STATE_ITEMS}
        label="State"
        name="state"
        onField={onField}
        value={fields.state}
      />
      <FindSelect
        items={EVERY_ITEMS}
        label="Every"
        name="every"
        onField={onField}
        value={fields.every}
      />
      <RangeField
        fields={fields}
        label="Velocity"
        max={127}
        min={1}
        name="velocity"
        onChange={handleChange}
      />
      <RangeField
        fields={fields}
        label="Length"
        max={64}
        min={1}
        name="length"
        onChange={handleChange}
      />
      <RangeField
        fields={fields}
        label="Chance %"
        max={100}
        min={0}
        name="chance"
        onChange={handleChange}
      />
      <div className="flex items-center gap-2 text-xs">
        <input
          checked={fields.rest}
          className="accent-primary"
          id={`${id}-rest`}
          name="rest"
          onChange={handleChange}
          type="checkbox"
        />
        <label htmlFor={`${id}-rest`}>The notes that do not match</label>
      </div>
      <Button onClick={find} size="sm" type="button">
        Select
      </Button>
    </div>
  );
}

/** The form in a popover, opened from a search button. */
function NoteFind({
  onFind,
  scale,
}: Pick<NoteFindFormProps, "onFind" | "scale">) {
  return (
    <Popover>
      <PopoverTrigger
        render={
          <Button aria-label="Find notes" size="icon" variant="outline" />
        }
      >
        <IconPlaceholder
          hugeicons="Search01Icon"
          lucide="SearchIcon"
          phosphor="MagnifyingGlassIcon"
          remixicon="RiSearchLine"
          tabler="IconSearch"
        />
      </PopoverTrigger>
      <PopoverContent align="end" className="w-72">
        <NoteFindForm onFind={onFind} scale={scale} />
      </PopoverContent>
    </Popover>
  );
}

export { NoteFind, NoteFindForm };
```

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</CodeTabs>

## Usage

```tsx
import { findNotes } from "@audio-ui/core/piano-roll";
import { NoteFind } from "@/components/audio-ui/note-find";

<NoteFind
  onFind={(query) => setSelection(findNotes(notes, query))}
  scale={scale}
/>;
```

`NoteFind` is a search button that opens the form in a popover. Pressing Select hands `onFind` a `NoteQuery`, and `findNotes` from `@audio-ui/core/piano-roll` returns the ids of the notes it matches, ready to be a [Piano roll](/docs/components/base/piano-roll)'s `selection`. The query is data, so it can also feed anything else that reads notes: a step sequencer, a list, an agent's tool.

Each field narrows the notes found, and an empty one sets no limit:

- **Pitch**: one pitch class, in any octave.
- **Scale**: in the scale or out of it. Off until `scale` is given.
- **State**: active notes, or deactivated ones.
- **Every**: of the notes found, in the order they play, every second, third or fourth.
- **Velocity** (1 to 127), **Length** (in steps) and **Chance %**: a range, from and to, either end left open.
- **The notes that do not match**: finds the rest instead.

### Without the popover

`NoteFindForm` is the form alone, to put in a sheet, a sidebar or a panel of your own:

```tsx
import { NoteFindForm } from "@/components/audio-ui/note-find";

<NoteFindForm onFind={selectFound} scale={scale} />;
```

## API reference

### NoteFind

<TypeTable>
<TypeRow name={"onFind"} type={"(query: NoteQuery) => void"}>Fired with the query when Select is pressed.</TypeRow>
<TypeRow name={"scale"} type={"Scale | null"}>The scale marked, which the Scale field reads. With `null`, that field is off.</TypeRow>
</TypeTable>

### NoteFindForm

Takes `onFind` and `scale`, and the props of a `div`.
