# Aperture — Animated UI Component for React

> A trigger button that irises open in place into a compact form — one continuous surface through invite, input, pending, and confirmation. Measured FLIP with radius continuity, fields fading up after the frame lands; while async onSubmit is in flight a thin conic arc sweeps the submit control, then the surface exhales into a confirmation chip and returns to rest. Real form semantics, full focus trap, Escape and outside-click reverse the morph, reduced-motion crossfades.

- Page: https://crucibleui.com/components/utility/aperture
- Category: [utility](https://crucibleui.com/components/utility)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/aperture
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/aperture`.

## Props (`ApertureProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `trigger?` | `React.ReactNode` | `"Feedback"` | Content of the resting trigger button. |
| `children?` | `React.ReactNode` | — | Form contents rendered inside the open surface. Omit for the built-in single-field + submit preset. Custom children should include an `<ApertureSubmit>` so the pending iris has a control to sweep. |
| `open?` | `boolean` | — | Controlled open state of the form. Omit for uncontrolled use. |
| `defaultOpen?` | `boolean` | `false` | Initial open state in uncontrolled mode. |
| `onOpenChange?` | `(open: boolean) => void` | — | Fires whenever the form asks to open or close (trigger click, Escape, outside click, submit). |
| `onSubmit?` | `(data: FormData, event: React.FormEvent<HTMLFormElement>) => void \| Promise<void>` | — | Called on submit with the form's FormData (the preset field is named `"message"`). Return a promise to drive the pending iris — resolve morphs to the confirmation chip, reject keeps the form open for retry. |
| `spring?` | `{ stiffness?: number; damping?: number; mass?: number }` | `{ stiffness: 340, damping: 32, mass: 0.9 }` | Morph spring overrides, merged over the default. |
| `successContent?` | `React.ReactNode` | `a drawn check + "Thank you"` | Confirmation chip content after a successful submit. |
| `autoReturnDelay?` | `number \| null` | `2200` | Milliseconds the confirmation chip rests before morphing back to the button. Pass `null` to disable auto-return (Escape or an outside click still dismisses the chip). |
| `direction?` | `ApertureDirection` | `"centered"` | Growth direction of the surface from the trigger. |
| `openWidth?` | `number` | `320` | Width of the open form in px. |
| `openHeight?` | `number` | — | Fixed height of the open form in px. Omit for auto height. |
| `accent?` | `string` | `"#a5b4fc"` | Accent hex — used on the submit control and focus rings only. |
| `fieldLabel?` | `string` | `"Your message"` | Label above the preset field. |
| `fieldPlaceholder?` | `string` | `"Share what's on your mind…"` | Placeholder of the preset field. |
| `fieldType?` | `"textarea" \| "text" \| "email"` | `"textarea"` | Preset field element/type. |
| `submitLabel?` | `string` | `"Send"` | Label of the preset submit control. |
| `closeOnEscape?` | `boolean` | `true` | Close (reverse the morph) on the Escape key. |
| `closeOnOutsideClick?` | `boolean` | `true` | Close when clicking outside the surface. |
| `className?` | `string` | — | Extra classes for the inline root wrapper. |

## Usage example

```tsx
"use client";

import { Aperture } from "@/components/crucible/aperture";

export default function ApertureDemo() {
  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.035),transparent_70%)]"
      />
      <div className="relative z-10 flex flex-col items-center gap-16">
        <Aperture
          trigger="Feedback"
          fieldLabel="Your message"
          fieldPlaceholder="What could be better?"
          onSubmit={() => new Promise((resolve) => setTimeout(resolve, 1400))}
        />
        <p className="text-sm text-neutral-500">
          Click the button — it opens in place. Submit and watch it confirm.
        </p>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.
