{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "selection-frame",
  "type": "registry:ui",
  "title": "Selection frame",
  "description": "The one selection for every kind of object: a 1.25 green ring with a flat collar at offset 6, eight handles on the ring line, and a graphite readout of the measured frame. Hover shows only faint corner dots and an edge light.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/selection-frame/selection-frame.tsx",
      "type": "registry:ui",
      "target": "components/metalui/selection-frame/selection-frame.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { flushSync } from 'react-dom';\nimport { SizeReadout } from '../size-readout/size-readout';\n\n/* ─────────────────────────────────────────────────────────\n * SELECTION FRAME (the reference's renderSelection, as one object)\n *\n * rest      nothing: a borderless object has no edge at rest\n * hover     faint corner dots where the handles will be (settle fade);\n *           an edge light on the side the pointer entered the band\n *           a soft glow behind the corner under the pointer (also while selected:\n *           it says \"resize from here\" behind the handle)\n * selected  ring 1.25 + collar 3.5 at offset 6 (radius + 6)\n *     0ms   ring and handles enter from 1.02, opacity 0, on the part spring\n *           (Reduce Motion: part resolves instant)\n *           the readout reads the measured frame: ● W × H\n *   typing  ring, handles and readout re-measure in the same frame as the\n *           host's layout (ResizeObserver runs before paint); the entrance\n *           never replays; the readout sits at .78\n *   moving  the readout returns to 1\n *   copied  the readout reads COPIED · PNG W × H for 900 ms\n * lite      one quiet 1 pt ring, no collar, no handles, no entrance: a\n *           member of a multi-selection, or a selection made by finishing\n * ───────────────────────────────────────────────────────── */\n\nexport type SelectionHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';\nexport type SelectionEdge = 'n' | 'e' | 's' | 'w';\n\nexport interface SelectionFrameProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {\n  /** rest (nothing), hover (corner dots) or selected (ring, handles, readout). */\n  state?: 'rest' | 'hover' | 'selected';\n  /** ring: the one selection. lite: a quiet ring (multi-selection member, or a selection made by finishing). */\n  variant?: 'ring' | 'lite';\n  /** writing dims the readout to .78; moving returns it to 1. */\n  mode?: 'idle' | 'writing' | 'moving';\n  /** The object's corner radius. The ring's is this plus the selection offset (6). */\n  radius?: number;\n  /**\n   * Which handles show. object: eight, all resize. text: corners and e/w set the width, n and s are\n   * grips that move the object. none: no handles.\n   */\n  handles?: 'object' | 'text' | 'none';\n  /** Show the readout under the object (default true while selected). */\n  readout?: boolean;\n  /** Blocks in a multi-selection: the readout reads \"● N · W × H\". */\n  count?: number;\n  /** The frame to read. Default: the host's measured border box, fractional, rounded for display. */\n  size?: { width: number; height: number };\n  /** Set to a format (e.g. \"PNG\") after a copy: the readout says so for 900 ms. */\n  copied?: string | null;\n  /** The band edge under the pointer: its edge light shows. */\n  edge?: SelectionEdge | null;\n  /** The band corner under the pointer: a soft glow shows behind it. */\n  corner?: 'nw' | 'ne' | 'se' | 'sw' | null;\n  /** Play the ring's entrance when it becomes selected (default true). */\n  entrance?: boolean;\n  /** A handle was pressed. The host resizes or moves the object; the frame follows its box. */\n  onHandlePointerDown?: (handle: SelectionHandle, event: React.PointerEvent<HTMLSpanElement>) => void;\n}\n\n/* Every value is the presence group. The frame fills its host (the host is position: relative) and\n * every part is placed from the host's box, so the ring tracks the object in the same frame; the parts\n * read the frame's state, variant and mode through the sf group. */\nconst FRAME = 'mu-selection-frame group/sf absolute inset-0 pointer-events-none presence-frame';\n/* 1.25 green at offset 6 and a flat 3.5 collar outside it, never a blur; lite is one quiet ring. It\n * enters once, from 1.02 on the part spring; resizing never replays it. */\nconst RING = 'mu-sf-ring absolute presence-ring opacity-0 group-data-[state=selected]/sf:opacity-100 group-data-[variant=lite]/sf:presence-ring-lite group-data-[state=selected]/sf:group-data-[variant=ring]/sf:group-data-entrance/sf:animate-sf-in';\n/* Hover: only the faint corner dots, where the handles will be. */\nconst DOT = 'mu-sf-dot absolute size-presence-hover-dot -translate-1/2 rounded-round bg-presence-dot opacity-0 transition-opacity duration-settle ease-settle group-data-[state=hover]/sf:opacity-100';\n/* The edge light: the pointer entered the band on one edge. */\nconst EDGE = 'mu-sf-edge absolute opacity-0 transition-opacity duration-settle ease-settle data-on:opacity-100';\nconst EDGE_AT = { n: 'presence-edge-n', e: 'presence-edge-e', s: 'presence-edge-s', w: 'presence-edge-w' };\n/* The corner glow: the pointer is over a corner of the band, selected or not. */\nconst GLOW = 'mu-sf-glow absolute -translate-1/2 presence-corner-glow opacity-0 transition-opacity duration-settle ease-settle data-on:opacity-presence-corner-glow';\n/* Handles on the ring line: round caps at the corners, capsules at the edge midpoints, each with a hit\n * area 7 wider; on text the n and s capsules are grips. */\nconst HANDLE = 'mu-sf-handle absolute -translate-1/2 rounded-pill pointer-events-auto touch-none after:absolute after:-inset-presence-handle-hit after:rounded-pill group-data-[state=selected]/sf:group-data-[variant=ring]/sf:group-data-entrance/sf:animate-sf-fade';\nconst SHAPE = {\n  nw: 'size-presence-handle', ne: 'size-presence-handle', se: 'size-presence-handle', sw: 'size-presence-handle',\n  e: 'w-presence-capsule-thickness h-presence-capsule-length', w: 'w-presence-capsule-thickness h-presence-capsule-length',\n  n: 'w-presence-capsule-length h-presence-capsule-thickness', s: 'w-presence-capsule-length h-presence-capsule-thickness',\n};\nconst RESIZE = {\n  nw: 'presence-handle cursor-nwse-resize', se: 'presence-handle cursor-nwse-resize',\n  ne: 'presence-handle cursor-nesw-resize', sw: 'presence-handle cursor-nesw-resize',\n  e: 'presence-handle cursor-ew-resize', w: 'presence-handle cursor-ew-resize',\n  n: 'presence-handle cursor-ns-resize', s: 'presence-handle cursor-ns-resize',\n};\nconst GRIP = 'presence-grip cursor-grab';\n/* The readout sits 16 under the object, centred; .78 while writing, 1 while moving. */\nconst READOUT = 'absolute presence-readout-at group-data-[mode=writing]/sf:opacity-presence-readout-writing group-data-[mode=moving]/sf:opacity-100';\n\nconst HANDLES: SelectionHandle[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];\n// Handle centres on the ring line, as fractions of the host box plus the offset outward.\nconst AT: Record<SelectionHandle, [number, number, number, number]> = {\n  nw: [0, 0, -1, -1], n: [0.5, 0, 0, -1], ne: [1, 0, 1, -1], e: [1, 0.5, 1, 0],\n  se: [1, 1, 1, 1], s: [0.5, 1, 0, 1], sw: [0, 1, -1, 1], w: [0, 0.5, -1, 0],\n};\nconst place = (h: SelectionHandle): React.CSSProperties => {\n  const [fx, fy, ox, oy] = AT[h];\n  return { left: `calc(${fx * 100}% + var(--o) * ${ox})`, top: `calc(${fy * 100}% + var(--o) * ${oy})` };\n};\n\n/**\n * The host's border box, fractional and untransformed (a lifted or zoomed host reads its layout size),\n * re-read in the same frame as the host's layout: ResizeObserver runs after layout and before paint,\n * and flushSync commits the new numbers before that paint.\n */\nfunction useHostSize(ref: React.RefObject<HTMLDivElement | null>, enabled: boolean) {\n  const [size, setSize] = React.useState<{ width: number; height: number } | null>(null);\n  React.useLayoutEffect(() => {\n    const host = ref.current?.parentElement;\n    if (!host || !enabled) return;\n    const same = (a: { width: number; height: number } | null, w: number, h: number) => !!a && a.width === w && a.height === h;\n    setSize((s) => (same(s, host.offsetWidth, host.offsetHeight) ? s : { width: host.offsetWidth, height: host.offsetHeight }));\n    const ro = new ResizeObserver(([entry]) => {\n      const b = entry.borderBoxSize?.[0];\n      const w = b ? b.inlineSize : host.offsetWidth, h = b ? b.blockSize : host.offsetHeight;\n      flushSync(() => setSize((s) => (same(s, w, h) ? s : { width: w, height: h })));\n    });\n    ro.observe(host, { box: 'border-box' });\n    return () => ro.disconnect();\n  }, [ref, enabled]);\n  return size;\n}\n\n/**\n * The one selection for every kind of object. Place it as the last child of the object\n * (position: relative). For a borderless object the ring and its handles are the boundary.\n */\nexport const SelectionFrame = React.forwardRef<HTMLDivElement, SelectionFrameProps>(function SelectionFrame(\n  {\n    state = 'rest', variant = 'ring', mode = 'idle', radius = 0, handles = 'object', readout = true, count, size,\n    copied, edge = null, corner = null, entrance = true, onHandlePointerDown, className, style, ...props\n  },\n  forwardedRef,\n) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  React.useImperativeHandle(forwardedRef, () => ref.current as HTMLDivElement);\n  const selected = state === 'selected';\n  const measured = useHostSize(ref, selected && readout && !size);\n  const box = size ?? measured;\n  const showHandles = selected && variant === 'ring' && handles !== 'none';\n\n  // \"COPIED\" holds for the token's 900 ms, then the readout reads the frame again.\n  const [copiedShown, setCopiedShown] = React.useState<string | null>(null);\n  React.useEffect(() => {\n    if (!copied) return;\n    setCopiedShown(copied);\n    const ms = parseFloat(getComputedStyle(ref.current ?? document.documentElement).getPropertyValue('--mu-presence-copied-ms')) || 900;\n    const t = setTimeout(() => setCopiedShown(null), ms);\n    return () => clearTimeout(t);\n  }, [copied]);\n\n  return (\n    <div\n      ref={ref}\n      aria-hidden\n      data-state={state}\n      data-variant={variant}\n      data-mode={mode}\n      data-entrance={entrance ? '' : undefined}\n      className={className ? `${FRAME} ${className}` : FRAME}\n      style={{ '--mu-sf-radius': `${radius}px`, ...style } as React.CSSProperties}\n      {...props}\n    >\n      <span className={RING} />\n      {state === 'hover' || state === 'rest'\n        ? (['nw', 'ne', 'se', 'sw'] as SelectionHandle[]).map((c) => <span key={c} className={DOT} style={place(c)} />)\n        : null}\n      {(['nw', 'ne', 'se', 'sw'] as const).map((c) => (\n        <span key={`g-${c}`} className={GLOW} data-corner={c} data-on={corner === c ? '' : undefined} style={place(c)} />\n      ))}\n      {(['n', 'e', 's', 'w'] as SelectionEdge[]).map((e) => (\n        <span key={e} className={`${EDGE} ${EDGE_AT[e]}`} data-edge={e} data-on={edge === e && state !== 'selected' ? '' : undefined} />\n      ))}\n      {showHandles &&\n        HANDLES.map((k) => {\n          const grip = handles === 'text' && (k === 'n' || k === 's');\n          return (\n            <span\n              key={k}\n              className={`${HANDLE} ${SHAPE[k]} ${grip ? GRIP : RESIZE[k]}`}\n              data-handle={k}\n              data-grip={grip ? '' : undefined}\n              title={grip ? 'Drag to move' : handles === 'text' ? 'Drag to set the width · double-click to fit the text' : undefined}\n              style={place(k)}\n              onPointerDown={onHandlePointerDown ? (e) => onHandlePointerDown(k, e) : undefined}\n            />\n          );\n        })}\n      {selected && readout && box && <SizeReadout className={READOUT} width={box.width} height={box.height} count={count} copied={copiedShown} />}\n    </div>\n  );\n});\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/selection-frame.md. SwiftUI: MetalSelectionFrame in the MetalUI Swift package."
}
