# Share this view (Notchset): prompt.md (v2.1.0)

- id: `share-view` · version 2.1.0 · block · free
- category: Marketing
- build: one set of files for every shadcn style
- install: `npx shadcn@latest add https://notchset.dev/r/share-view.json`
- npm dependencies: lucide-react@^1
- registry dependencies: utils, https://notchset.dev/r/notchset-foundation.json, @notchset/button, @notchset/icon-button, @notchset/recipient-field, @notchset/segmented-control
- docs: https://notchset.dev/blocks/share-view
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

The Share pill opens into the panel it was hiding, in place: copy the link, pick who can open it, add people and Send. A sent share folds back into a pill that says who has it; a failed one keeps everything and offers Retry..

## Build it
- Stack: React 18.3 or 19, TypeScript, Tailwind CSS v4 utility classes on the foundation's roles, a components.json (imports use its aliases).
- Packages: `lucide-react@^1`.
- Files: `components/ui/notchset/blocks/share-view.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`, `button`, `icon-button`, `recipient-field`, `segmented-control`.
- Builds: one set of files, the same for every shadcn style.
- Exports to keep: `ShareView`, and every exported type.
- CSS: the install adds the notchset foundation (its roles, keyframes and motion switch, reading your shadcn tokens for colour) to your global stylesheet once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- This item reads `--ns-bad-ink`, `--ns-bezel`, `--ns-ease`, `--ns-ease-panel`, `--ns-hover`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-ok`, `--ns-panel`, `--ns-radius`, `--ns-radius-xl`, `--ns-shadow-float`, `--ns-signal`, `--ns-text-control`, `--ns-text-small`, `--ns-text-strong`, `--ns-time-color`, `--ns-time-dialog`, `--ns-time-page`, `--ns-weight-medium`, `--ns-well`.
- Retune it (on any ancestor) with the foundation's roles and hooks: `--ns-bad`, `--ns-bad-ink`, `--ns-band`, `--ns-band-edge`, `--ns-band-grade`, `--ns-band-optimal`, `--ns-beat`, `--ns-bezel`, `--ns-canvas`, `--ns-cat-1`, `--ns-cat-2`, `--ns-cat-3`, `--ns-cat-4`, `--ns-cat-5`, `--ns-cat-6`, `--ns-check`, `--ns-chip`, `--ns-control`, `--ns-control-pad`, `--ns-control-sm`, `--ns-deep`, `--ns-deep-ink`, `--ns-deep-ink-2`, `--ns-deep-line`, `--ns-dialog`, `--ns-dialog-lg`, `--ns-dialog-sm`, `--ns-ease`, `--ns-ease-in`, `--ns-ease-panel`, `--ns-ease-pop`, `--ns-field`, `--ns-flood-arc`, `--ns-flood-aurora`, `--ns-flood-behind-1`, `--ns-flood-behind-2`, `--ns-flood-bloom`, `--ns-flood-deep`, `--ns-flood-dial-bloom`, `--ns-flood-glass`, `--ns-flood-grain`, `--ns-flood-image`, `--ns-flood-ink`, `--ns-flood-ink-low`, `--ns-flood-light`, `--ns-flood-mid`, `--ns-flood-pale`, `--ns-flood-pane`, `--ns-flood-pane-edge`, `--ns-flood-pill-ink`, `--ns-flood-ribbon`, `--ns-flood-ribbon-core`, `--ns-flood-shadow`, `--ns-flood-sheen`, `--ns-flood-wash`, `--ns-font-body`, `--ns-font-display`, `--ns-font-mono`, `--ns-font-script`, `--ns-glass`, `--ns-glass-blur`, `--ns-glass-shadow`, `--ns-glow`, `--ns-grain`, `--ns-gutter`, `--ns-header`, `--ns-heat`, `--ns-hover`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-ink-4`, `--ns-leading`, `--ns-lift`, `--ns-light-1`, `--ns-light-2`, `--ns-light-3`, `--ns-light-spread`, `--ns-light-strength`, `--ns-line`, `--ns-line-soft`, `--ns-lit-edge`, `--ns-menu-dy`, `--ns-menu-row`, `--ns-mesh`, `--ns-mesh-frost`, `--ns-ok`, `--ns-ok-ink`, `--ns-orb-body`, `--ns-orb-cool`, `--ns-orb-core`, `--ns-orb-dawn`, `--ns-orb-deep`, `--ns-orb-edge`, `--ns-orb-grain`, `--ns-orb-ground`, `--ns-orb-halo`, `--ns-orb-inner`, `--ns-orb-key`, `--ns-orb-lift`, `--ns-orb-limb`, `--ns-orb-limb-from`, `--ns-orb-rim`, `--ns-orb-skin-hi`, `--ns-orb-skin-lo`, `--ns-orb-skin-mid`, `--ns-orb-warm`, `--ns-panel`, `--ns-panel-w`, `--ns-prism-edge`, `--ns-prism-glint`, `--ns-prism-sheen`, `--ns-prism-tint`, `--ns-pulse`, `--ns-radius`, `--ns-radius-card`, `--ns-radius-full`, `--ns-radius-lg`, `--ns-radius-sm`, `--ns-radius-xl`, `--ns-raised`, `--ns-range`, `--ns-row`, `--ns-row-compact`, `--ns-segment`, `--ns-selected`, `--ns-series`, `--ns-series-core`, `--ns-series-lilac`, `--ns-series-tint`, `--ns-shadow-dialog`, `--ns-shadow-float`, `--ns-shadow-panel`, `--ns-sheet`, `--ns-sheet-bottom`, `--ns-side`, `--ns-signal`, `--ns-sleep-awake`, `--ns-sleep-deep`, `--ns-sleep-light`, `--ns-sleep-rem`, `--ns-space-1`, `--ns-space-2`, `--ns-space-3`, `--ns-space-4`, `--ns-space-5`, `--ns-space-6`, `--ns-space-8`, `--ns-space-section`, `--ns-stack-on`, `--ns-standing-critical`, `--ns-standing-in`, `--ns-standing-out`, `--ns-state-doing`, `--ns-state-done`, `--ns-state-review`, `--ns-state-todo`, `--ns-switch-h`, `--ns-switch-w`, `--ns-text-body`, `--ns-text-control`, `--ns-text-display`, `--ns-text-figure`, `--ns-text-headline`, `--ns-text-hero`, `--ns-text-lede`, `--ns-text-meta`, `--ns-text-page`, `--ns-text-record`, `--ns-text-small`, `--ns-text-strong`, `--ns-text-sub`, `--ns-text-title`, `--ns-tile-shadow`, `--ns-time-born`, `--ns-time-color`, `--ns-time-dialog`, `--ns-time-menu`, `--ns-time-page`, `--ns-time-panel`, `--ns-time-toast`, `--ns-tint`, `--ns-tint-ink`, `--ns-toast-bottom`, `--ns-toast-left`, `--ns-touch`, `--ns-tracking-display`, `--ns-tracking-headline`, `--ns-warn`, `--ns-warn-ink`, `--ns-wash-clay`, `--ns-wash-dusk`, `--ns-wash-graphite`, `--ns-wash-ink`, `--ns-wash-sage`, `--ns-wash-sand`, `--ns-weight-light`, `--ns-weight-medium`, `--ns-weight-regular`, `--ns-weight-strong`, `--ns-well`.
- Never add Tailwind colour classes inside the component.

```tsx
import { ShareView } from "@/components/ui/notchset/blocks/share-view";
```

## Parts
- A single export that takes props.

## Match the original
- Read `components/ui/notchset/blocks/share-view.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when

### Not when

## Built from
- button (`button`), installed with it: reuse it, don't rebuild it
- icon-button (`icon-button`), installed with it: reuse it, don't rebuild it
- recipient-field (`recipient-field`), installed with it: reuse it, don't rebuild it
- segmented-control (`segmented-control`), installed with it: reuse it, don't rebuild it

## Mistakes
- Recreating the component instead of installing it

## Usage

```tsx
"use client";
import * as React from "react";
import { ShareView } from "@/components/ui/notchset/blocks/share-view";
import { Switch } from "@/components/ui/notchset/switch";

const TEAM = [
  { id: "u1", name: "Priya Raman", email: "priya@larkspur.io", hue: 280, sub: "Sales lead" },
  { id: "u2", name: "Omar Haddad", email: "omar@larkspur.io", hue: 160, sub: "Account executive" },
  { id: "u3", name: "Kevin Ito", email: "kevin@larkspur.io", hue: 210, sub: "Finance" },
  { id: "u4", name: "Ana Ruiz", email: "ana@larkspur.io", hue: 20, sub: "CEO" },
];

export default function ShareViewDemo() {
  const [fails, setFails] = React.useState(false);
  return (
    <div className="flex w-full max-w-[440px] flex-col gap-3">
      <ShareView
        view="Q4 commit, EMEA"
        link="https://app.example.com/v/q4-commit-emea"
        people={TEAM}
        defaultTo={[TEAM[0]!, TEAM[2]!]}
        access={[
          { value: "team", label: "Team" },
          { value: "invited", label: "Only invited" },
        ]}
        /* resolve false and nobody is told: the panel keeps the people and offers Retry */
        onSend={() => new Promise<boolean>((r) => setTimeout(() => r(!fails), 600))}
      />
      <label className="flex items-center gap-2 self-end text-[length:var(--ns-text-small)] text-[var(--ns-ink-3)]">
        Sending fails
        <Switch checked={fails} onCheckedChange={setFails} aria-label="Make sending fail" />
      </label>
    </div>
  );
}
```

This example also uses `switch` (https://notchset.dev/components/switch). Installing this item doesn't bring them; to paste the example as it is: `npx shadcn@latest add https://notchset.dev/r/share-view.json https://notchset.dev/r/switch.json`.

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `view` | `string` |  | the view's name, said in the panel's title |
| `link` | `string` |  | Required. |
| `people` | `Recipient[]` |  | Required. |
| `access` | `{ value: string; label: string }[]` |  | Required. |
| `defaultTo` | `Recipient[]` | `[]` | people already chosen when it opens |
| `onSend` | `(to: Recipient[], access: string) => Promise<boolean \| void> \| boolean \| void` |  | the host's send; reject (or resolve false) to refuse it |
| `align` | `"center" \| "end"` | `"center"` | where the pill sits and the panel grows from: the middle, or the end of a toolbar |

Full docs: https://notchset.dev/blocks/share-view

## Customising
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- Dark mode follows the `.dark` class or `data-theme="dark"` on an ancestor (the shadcn and next-themes conventions).
- Update later: Commit or stash local changes first (or copy the files), so the update can be undone. Read https://notchset.dev/r/changelog.json for the new versions: a major version, an entry marked breaking or a merge says what to change. Then re-run the install with `--overwrite`, on a branch. It replaces the component's files, the shared helper files they import, and the item's CSS rules in the global stylesheet, so review the whole diff (components, shared files, CSS), re-apply your own edits, and run the app's typecheck, tests and build.

## Accessibility contract (preserve when editing)

## Install

```bash
npx shadcn@latest add https://notchset.dev/r/share-view.json
```

Or add the `@notchset` registry to components.json and run `npx shadcn@latest add @notchset/share-view`. Every components.json style gets the same files.

## Source

### components/ui/notchset/blocks/share-view.tsx

```tsx
// Notchset share-view 2.1.0 · https://notchset.dev/changelog
"use client";
import * as React from "react";
import { Check, Link2, Share2, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/notchset/button";
import { IconButton } from "@/components/ui/notchset/icon-button";
import { RecipientField, type Recipient } from "@/components/ui/notchset/recipient-field";
import { SegmentedControl } from "@/components/ui/notchset/segmented-control";

/*
 * Share this view: the Share pill opens into the panel it was hiding, in place, so you never lose where you were.
 * Copy the link (it says Copied only when it was; otherwise the link is selected to copy by hand), pick who can
 * open it, add people as chips and Send. A send that works closes
 * the panel back into the pill, which now says who has it; a send that fails keeps the panel, the people and the
 * note, says nobody was told, and offers Retry. Escape or the close button folds it back and returns focus to
 * the pill.
 *
 *   <ShareView view="Q4 commit, EMEA" link="https://app.example.com/v/q4-emea" people={team}
 *     access={[{ value: "team", label: "Team" }, { value: "invited", label: "Only invited" }]} onSend={(to, access) => send(to, access)} />
 */
type ShareViewProps = {
  /** the view's name, said in the panel's title */
  view: string;
  link: string;
  people: Recipient[];
  access: { value: string; label: string }[];
  /** people already chosen when it opens */
  defaultTo?: Recipient[] | undefined;
  /** the host's send; reject (or resolve false) to refuse it */
  onSend?: ((to: Recipient[], access: string) => Promise<boolean | void> | boolean | void) | undefined;
  /** where the pill sits and the panel grows from: the middle, or the end of a toolbar */
  align?: "center" | "end" | undefined;
  className?: string | undefined;
};

const PILL_W = 104, PILL_H = 36;
/* before paint in the browser; on the server (where layout effects warn) an ordinary effect */
const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;

function ShareView({ view, link, people, access: levels, defaultTo = [], onSend, align = "center", className }: ShareViewProps) {
  const [open, setOpen] = React.useState(false);
  const [to, setTo] = React.useState<Recipient[]>(defaultTo);
  const [access, setAccess] = React.useState(levels[0]?.value ?? "");
  const [copied, setCopied] = React.useState(false);
  const [state, setState] = React.useState<"idle" | "busy" | "failed">("idle");
  const [sent, setSent] = React.useState<Recipient[]>([]);
  const [h, setH] = React.useState(PILL_H);
  const inner = React.useRef<HTMLDivElement>(null);
  const linkText = React.useRef<HTMLSpanElement>(null);
  const pill = React.useRef<HTMLButtonElement>(null);
  const root = React.useRef<HTMLDivElement>(null);
  const titleId = React.useId();
  /* the share is this view at this link: given another, it starts as that one's own (its default people, first
     access, nothing sent), and an answer still on its way for the one before lands nowhere */
  const share = `${view}\n${link}`;
  const [shown, setShown] = React.useState(share);
  if (shown !== share) {
    setShown(share);
    setTo(defaultTo);
    setAccess(levels[0]?.value ?? "");
    setState("idle");
    setSent([]);
    setCopied(false);
  }
  /* a generation per share change: A, then B, then A again is a new A, so the first A's answer still lands nowhere */
  const generation = React.useRef(0);
  useIsomorphicLayoutEffect(() => {
    generation.current += 1;
  }, [share]);
  /* a send belongs to the share it was made from, not to the panel: it outlives Close, its people and access are
     locked while it runs, and when it lands it only folds a panel that is still open, taking focus back to the
     pill only if focus was still in this share. Unmounted, or the share replaced by another view, it changes nothing */
  const isOpen = React.useRef(open);
  React.useEffect(() => {
    isOpen.current = open;
  }, [open]);
  const alive = React.useRef(true);
  React.useEffect(() => {
    alive.current = true;
    return () => void (alive.current = false);
  }, []);
  /* opening hands focus to the panel's first field, so Escape and Tab start inside it */
  React.useEffect(() => {
    if (open) requestAnimationFrame(() => inner.current?.querySelector<HTMLElement>("input, button:not([disabled])")?.focus({ preventScroll: true }));
  }, [open]);

  /* the panel's height follows what it holds, so a chip or a failure line grows it smoothly */
  React.useEffect(() => {
    const el = inner.current;
    if (!open || !el) return;
    const fit = () => setH(el.offsetHeight);
    fit();
    if (typeof ResizeObserver === "undefined") return;
    const ro = new ResizeObserver(fit);
    ro.observe(el);
    return () => ro.disconnect();
  }, [open]);

  const close = () => {
    setOpen(false);
    setH(PILL_H);
    requestAnimationFrame(() => {
      const a = document.activeElement;
      if (!a || a === document.body || !!root.current?.contains(a)) pill.current?.focus();
    });
  };
  const copyGen = React.useRef(0);
  const copyTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined);
  React.useEffect(() => {
    copyGen.current += 1;
    clearTimeout(copyTimer.current);
  }, [share]);
  React.useEffect(() => () => clearTimeout(copyTimer.current), []);
  const copy = async () => {
    const mine = ++copyGen.current;
    try {
      if (!navigator.clipboard) throw new Error("no clipboard");
      await navigator.clipboard.writeText(link);
    } catch {
      if (copyGen.current !== mine) return;
      /* not copied, so never "Copied": the link is selected to copy by hand */
      const el = linkText.current, sel = window.getSelection();
      if (el && sel) {
        const r = document.createRange();
        r.selectNodeContents(el);
        sel.removeAllRanges();
        sel.addRange(r);
      }
      return;
    }
    if (!alive.current || copyGen.current !== mine) return;
    setCopied(true);
    clearTimeout(copyTimer.current);
    copyTimer.current = setTimeout(() => setCopied(false), 1800);
  };
  const send = async () => {
    if (!to.length || state === "busy") return;
    const mine = generation.current;
    setState("busy");
    let ok = true;
    try {
      ok = (await onSend?.(to, access)) !== false;
    } catch {
      ok = false;
    }
    if (!alive.current || generation.current !== mine) return;
    if (!ok) return setState("failed");
    setState("idle");
    setSent(to);
    setTo([]);
    if (isOpen.current) close();
  };
  const who = (xs: Recipient[]) => (xs.length === 1 ? xs[0]!.name.split(" ")[0] : `${xs.length} people`);

  return (
    <div ref={root} data-slot="share-view" className={cn("flex w-full flex-col", align === "end" ? "items-end" : "items-center", className)}>
      <div
        data-ns-glide=""
        data-open={open || undefined}
        className={cn(
          "relative overflow-hidden bg-[var(--ns-panel)] [transition:width_var(--ns-time-page)_var(--ns-ease-panel),height_var(--ns-time-page)_var(--ns-ease-panel),border-radius_var(--ns-time-page)_var(--ns-ease-panel),box-shadow_var(--ns-time-color)]",
          open ? "rounded-[var(--ns-radius-xl)] shadow-[var(--ns-shadow-float)]" : "rounded-[18px] shadow-[var(--ns-bezel)]",
        )}
        style={{ width: open ? "100%" : PILL_W + (sent.length ? 40 : 0), maxWidth: 420, height: open ? h : PILL_H }}
      >
        {!open ? (
          <button
            ref={pill}
            type="button"
            aria-expanded={false}
            aria-label={sent.length ? `Share. Shared with ${who(sent)}` : "Share"}
            onClick={() => {
              setOpen(true);
              /* reopened while a send runs, it shows that send, still locked */
              setState((s) => (s === "busy" ? s : "idle"));
            }}
            className="flex size-full cursor-pointer items-center justify-center gap-1.5 text-[length:var(--ns-text-control)] font-[number:var(--ns-weight-medium)] text-[var(--ns-ink)] outline-hidden hover:bg-[var(--ns-hover)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--ns-signal)] [&_svg]:size-3.5"
          >
            {sent.length ? <Check className="text-[var(--ns-ok)]" /> : <Share2 />}
            {sent.length ? `Shared · ${who(sent)}` : "Share"}
          </button>
        ) : (
          <div
            ref={inner}
            role="dialog"
            aria-labelledby={titleId}
            onKeyDown={(e) => e.key === "Escape" && (e.stopPropagation(), close())}
            data-ns-moves=""
            className="absolute inset-x-0 top-0 flex flex-col gap-3 p-4 [animation:ns-island-in_var(--ns-time-dialog)_var(--ns-ease)_120ms_both]"
          >
            <div className="flex items-center gap-2">
              <span id={titleId} className="min-w-0 flex-1 truncate text-[length:var(--ns-text-strong)] font-[number:var(--ns-weight-medium)] text-[var(--ns-ink)]">
                Share &ldquo;{view}&rdquo;
              </span>
              <IconButton label="Close" size="sm" variant="ghost" tooltip={false} onClick={close}>
                <X />
              </IconButton>
            </div>
            <div className="flex items-center gap-2 rounded-[var(--ns-radius)] bg-[var(--ns-well)] py-1 pr-1 pl-2.5">
              <Link2 aria-hidden className="size-3.5 shrink-0 text-[var(--ns-ink-3)]" />
              <span ref={linkText} className="min-w-0 flex-1 truncate text-[length:var(--ns-text-small)] text-[var(--ns-ink-2)] select-all">{link}</span>
              <Button size="sm" onClick={() => void copy()} aria-live="polite">
                {copied ? <Check className="text-[var(--ns-ok)]" /> : null}
                {copied ? "Copied" : "Copy link"}
              </Button>
            </div>
            <fieldset disabled={state === "busy"} className="m-0 flex min-w-0 flex-col gap-3 border-0 p-0">
              <div className="flex flex-wrap items-center justify-between gap-2 text-[length:var(--ns-text-small)] text-[var(--ns-ink-2)]">
                Who can open it
                <SegmentedControl label="Who can open it" size="sm" options={levels} value={access} onValueChange={setAccess} />
              </div>
              <RecipientField aria-label="Send to" placeholder="Add people to tell" people={people} value={to} onValueChange={(v) => (setTo(v), state === "failed" && setState("idle"))} />
            </fieldset>
            <div aria-live="polite" className="flex items-center justify-end gap-3">
              {state === "failed" && (
                <span data-ns-moves="" className="min-w-0 flex-1 text-[length:var(--ns-text-small)] text-[var(--ns-bad-ink)] [animation:ns-island-in_var(--ns-time-dialog)_var(--ns-ease)_both]">
                  Couldn&rsquo;t send. Nobody was told; the people are kept.
                </span>
              )}
              <Button variant="primary" disabled={!to.length} busy={state === "busy"} onClick={() => void send()}>
                {state === "busy" ? "Sending…" : state === "failed" ? "Retry" : to.length ? `Send to ${who(to)}` : "Send"}
              </Button>
            </div>
          </div>
        )}
      </div>
      <span aria-live="polite" className="sr-only">
        {sent.length && !open ? `Sent to ${sent.map((p) => p.name).join(", ")}` : ""}
      </span>
    </div>
  );
}

export { ShareView, type ShareViewProps };
```

The foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://notchset.dev/r/notchset-foundation.json.
