{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "selection-frame",
  "title": "Selection Frame",
  "author": "AlgorizeTech",
  "description": "That box Figma draws around a selected layer — corner handles and a crisp accent stroke — around any text span or block. Use it to spotlight a word, a card or a screenshot region in docs and marketing.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/SelectionFrameBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport React, { useLayoutEffect, useRef, useState } from \"react\";\nimport { springLinear } from \"./motion-curves\";\n\nexport type FrameHandles = \"square\" | \"circle\";\nexport type FrameLine = \"solid\" | \"dashed\" | \"long\";\n\nexport interface SelectionFrameProps {\n  /** The content to frame — measured live, so it can change size freely. */\n  children: React.ReactNode;\n  /** Corner handle shape. Default \"square\". */\n  handles?: FrameHandles;\n  /** Stroke style. Default \"solid\". \"dashed\" and \"long\" both march when animated. */\n  line?: FrameLine;\n  /** Override the [dash, gap] px pair a non-solid `line` resolves to. */\n  dash?: [number, number];\n  /** Marching ants — only applies to \"dashed\" / \"long\" lines. Handles always pop in. Default false. */\n  animated?: boolean;\n  /** Accent colour. Default primary theme color. */\n  color?: string;\n  /** Frame corner radius. Default 0. */\n  radius?: number;\n  /** Gap between the content and the frame. Default 4. */\n  padding?: number;\n  /** Render inline (for text spans) instead of block. Default true. */\n  inline?: boolean;\n  className?: string;\n}\n\nconst STROKE = 1.5;\n\nconst LINE_DASH: Record<FrameLine, [number, number] | undefined> = {\n  solid: undefined,\n  dashed: [4, 4],\n  long: [10, 6],\n};\n\n/** Handle pop-in: a stiffness 600 / damping 24 spring, as a CSS easing. */\nconst HANDLE_SPRING = springLinear({ stiffness: 600, damping: 24 }, 120);\n\n// Plain CSS. Both loops sit behind prefers-reduced-motion: the march is an\n// indefinite loop, and the handles then simply start in place.\nconst KEYFRAMES = `\n@keyframes selection-frame-march {\n  from { stroke-dashoffset: 0px; }\n  to { stroke-dashoffset: calc(var(--selection-frame-period) * -1); }\n}\n@keyframes selection-frame-handle {\n  from { opacity: 0; transform: scale(0); }\n  to { opacity: 1; transform: scale(1); }\n}\n@media (prefers-reduced-motion: no-preference) {\n  .selection-frame-march { animation: selection-frame-march 1s linear infinite; }\n  .selection-frame-handle { animation: selection-frame-handle ${HANDLE_SPRING.duration}ms ${HANDLE_SPRING.easing} backwards; }\n}`;\n\nexport function SelectionFrame({\n  children,\n  handles = \"square\",\n  line = \"solid\",\n  dash,\n  animated = false,\n  color = \"var(--primary, #0d99ff)\",\n  radius = 0,\n  padding = 4,\n  inline = true,\n  className,\n}: SelectionFrameProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const [size, setSize] = useState({ w: 0, h: 0 });\n\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const ro = new ResizeObserver(() => setSize({ w: el.offsetWidth, h: el.offsetHeight }));\n    ro.observe(el);\n    setSize({ w: el.offsetWidth, h: el.offsetHeight });\n    return () => ro.disconnect();\n  }, []);\n\n  // The frame box sits `padding` outside the content, stroke centred on its edge.\n  const fw = size.w + padding * 2;\n  const fh = size.h + padding * 2;\n  const resolvedDash = dash ?? LINE_DASH[line];\n  const period = resolvedDash ? resolvedDash[0] + resolvedDash[1] : 0;\n\n  // Marching ants need a dash pattern, so `animated` with a solid stroke is\n  // just a static frame with animated corner handles. Reduced motion disables\n  // both (in the stylesheet above), since the march is an indefinite loop.\n  const marching = animated && !!resolvedDash;\n  const animateHandles = animated;\n\n  // The rect is drawn inset by STROKE/2 so its stroke centres inside the SVG\n  // box — its visible corner tip is at (STROKE/2, STROKE/2), not (0, 0). Place\n  // the handles on those centreline corners; using the box corners instead\n  // leaves every handle 0.75px diagonally outward from the tip it marks.\n  const half = STROKE / 2;\n  const corners: Array<[string, number, number]> = [\n    [\"tl\", half, half],\n    [\"tr\", fw - half, half],\n    [\"bl\", half, fh - half],\n    [\"br\", fw - half, fh - half],\n  ];\n\n  const handleSize = handles === \"circle\" ? 8 : 7;\n\n  return (\n    <span\n      className={cn(\"relative\", inline ? \"inline-block\" : \"block\", className)}\n      style={{ lineHeight: inline ? undefined : 0 }}\n    >\n      <span ref={ref} className={cn(inline ? \"inline-block\" : \"block\")}>\n        {children}\n      </span>\n\n      {fw > 0 && (\n        <>\n          {/* frame */}\n          <svg\n            className=\"absolute pointer-events-none overflow-visible\"\n            width={fw}\n            height={fh}\n            style={{ left: -padding, top: -padding }}\n            aria-hidden\n          >\n            <rect\n              x={STROKE / 2}\n              y={STROKE / 2}\n              width={Math.max(0, fw - STROKE)}\n              height={Math.max(0, fh - STROKE)}\n              rx={radius}\n              fill=\"none\"\n              stroke={color}\n              strokeWidth={STROKE}\n              strokeDasharray={resolvedDash ? `${resolvedDash[0]} ${resolvedDash[1]}` : undefined}\n              {...(marching\n                ? {\n                    className: \"selection-frame-march\",\n                    style: {\n                      \"--selection-frame-period\": `${period}px`,\n                      animationDuration: `${period * 0.075}s`,\n                    } as React.CSSProperties,\n                  }\n                : {})}\n            />\n          </svg>\n\n          {/* Corner handles. boxSizing: border-box is what keeps these centred on\n              their corner point: without it the border adds outside the declared\n              width/height, so the -handleSize/2 margin offset undershoots and the\n              handle renders visibly off-centre from the true corner. */}\n          {corners.map(([key, cx, cy], i) => (\n            <FrameHandle\n              key={key}\n              pop={animateHandles}\n              delay={0.28 + i * 0.05}\n              style={{\n                left: cx - padding,\n                top: cy - padding,\n                width: handleSize,\n                height: handleSize,\n                marginLeft: -handleSize / 2,\n                marginTop: -handleSize / 2,\n                boxSizing: \"border-box\",\n                border: `${STROKE}px solid ${color}`,\n                borderRadius: handles === \"circle\" ? \"9999px\" : 1,\n                boxShadow: \"0 1px 2px rgba(0,0,0,0.12)\",\n              }}\n            />\n          ))}\n        </>\n      )}\n      <style>{KEYFRAMES}</style>\n    </span>\n  );\n}\n\n/**\n * One corner handle. Whether it pops in is settled when it mounts: switching\n * `animated` on later leaves a handle that is already on screen in place.\n */\nfunction FrameHandle({\n  pop,\n  delay,\n  style,\n}: {\n  pop: boolean;\n  /** Seconds. */\n  delay: number;\n  style: React.CSSProperties;\n}) {\n  const [popIn] = useState(pop);\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        \"absolute pointer-events-none bg-background\",\n        popIn && \"selection-frame-handle\",\n      )}\n      style={{ animationDelay: popIn ? `${delay}s` : undefined, ...style }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/SelectionFrameBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/motion-curves.ts",
      "content": "/**\n * Motion curves with no animation library behind them.\n *\n * The components here ship as CSS, GSAP or Motion builds that must move\n * identically, so the curves are computed once, from the same numbers, and\n * handed to whichever engine runs them:\n *\n *   - `cubicBezier(x1, y1, x2, y2)` → an easing function (GSAP accepts any\n *     `(progress) => value` as `ease`), exact for Motion's `ease: [..]` arrays\n *     and for CSS `cubic-bezier()`.\n *   - `spring({ stiffness, damping, mass })` → the analytic solution Motion\n *     Motion uses, as an easing function plus the time it takes to settle, or\n *     as a CSS `linear()` string for a fixed start and end.\n *   - `createSpringFollower` → a value that chases a moving target through the\n *     same spring every frame, like Motion's `useSpring`, for pointer-driven\n *     motion that no fixed curve can describe.\n */\n\n// ---------------------------------------------------------------------------\n// Cubic bezier\n// ---------------------------------------------------------------------------\n\n/**\n * The CSS / Motion cubic-bezier as a function of progress. Newton-Raphson\n * with a bisection fallback, the same solver browsers use.\n */\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number) {\n  if (x1 === y1 && x2 === y2) return (t: number) => t;\n\n  const a = (p1: number, p2: number) => 1 - 3 * p2 + 3 * p1;\n  const b = (p1: number, p2: number) => 3 * p2 - 6 * p1;\n  const c = (p1: number) => 3 * p1;\n  const at = (t: number, p1: number, p2: number) =>\n    ((a(p1, p2) * t + b(p1, p2)) * t + c(p1)) * t;\n  const slope = (t: number, p1: number, p2: number) =>\n    3 * a(p1, p2) * t * t + 2 * b(p1, p2) * t + c(p1);\n\n  const solveX = (x: number) => {\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const s = slope(t, x1, x2);\n      if (Math.abs(s) < 1e-6) break;\n      const err = at(t, x1, x2) - x;\n      if (Math.abs(err) < 1e-7) return t;\n      t -= err / s;\n    }\n    let lo = 0;\n    let hi = 1;\n    t = x;\n    for (let i = 0; i < 30 && hi - lo > 1e-7; i++) {\n      const value = at(t, x1, x2);\n      if (value < x) lo = t;\n      else hi = t;\n      t = (lo + hi) / 2;\n    }\n    return t;\n  };\n\n  return (x: number) => (x <= 0 ? 0 : x >= 1 ? 1 : at(solveX(x), y1, y2));\n}\n\n/** Motion's named eases, as the cubic-beziers they are. */\nexport const MOTION_EASE = {\n  linear: [0, 0, 1, 1],\n  easeIn: [0.42, 0, 1, 1],\n  easeOut: [0, 0, 0.58, 1],\n  easeInOut: [0.42, 0, 0.58, 1],\n  circIn: [0.55, 0, 1, 0.45],\n  circOut: [0.55, 0, 0.45, 1],\n  circInOut: [0.85, 0, 0.15, 1],\n  backIn: [0.31, 0.01, 0.66, -0.59],\n  backOut: [0.33, 1.53, 0.69, 0.99],\n  anticipate: [0.66, -0.56, 0.34, 1.35],\n} as const;\n\n/**\n * Motion's default layout transition (`layout` / `layoutId`):\n * `{ duration: 0.45, ease: [0.4, 0, 0.1, 1] }`.\n */\nexport const LAYOUT_TRANSITION = { duration: 0.45, ease: [0.4, 0, 0.1, 1] as const };\n\n// ---------------------------------------------------------------------------\n// Spring\n// ---------------------------------------------------------------------------\n\nexport interface SpringOptions {\n  /** Motion default 100. */\n  stiffness?: number;\n  /** Motion default 10. */\n  damping?: number;\n  /** Motion default 1. */\n  mass?: number;\n  /** Starting velocity, in units of the whole distance per second. */\n  velocity?: number;\n  /** How close to the end counts as settled, as a fraction of the distance. */\n  restDelta?: number;\n}\n\n/**\n * Normalised position (0 → 1) of a spring released from 0 towards 1, at time\n * `t` seconds. The closed-form solution Motion's `spring()` generator\n * uses for the under-, critically and over-damped cases.\n */\nexport function springAt(options: SpringOptions = {}) {\n  const { stiffness = 100, damping = 10, mass = 1, velocity = 0 } = options;\n  const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n  const omega0 = Math.sqrt(stiffness / mass);\n  // Motion's convention: distance still to cover, velocity toward it negated.\n  const delta = 1;\n  const v0 = -velocity;\n\n  if (zeta < 1) {\n    const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);\n    return (t: number) => {\n      const envelope = Math.exp(-zeta * omega0 * t);\n      return (\n        1 -\n        envelope *\n          (((v0 + zeta * omega0 * delta) / omegaD) * Math.sin(omegaD * t) +\n            delta * Math.cos(omegaD * t))\n      );\n    };\n  }\n  if (zeta === 1) {\n    return (t: number) => 1 - Math.exp(-omega0 * t) * (delta + (v0 + omega0 * delta) * t);\n  }\n  const omegaD = omega0 * Math.sqrt(zeta * zeta - 1);\n  return (t: number) => {\n    const envelope = Math.exp(-zeta * omega0 * t);\n    const freq = Math.min(omegaD * t, 300);\n    return (\n      1 -\n      (envelope *\n        ((v0 + zeta * omega0 * delta) * Math.sinh(freq) + omegaD * delta * Math.cosh(freq))) /\n        omegaD\n    );\n  };\n}\n\n/** Seconds until the spring stays within `restDelta` of its end. */\nexport function springDuration(options: SpringOptions = {}) {\n  const at = springAt(options);\n  const restDelta = options.restDelta ?? 0.001;\n  const step = 1 / 240;\n  let lastOutside = 0;\n  for (let t = 0; t <= 10; t += step) {\n    if (Math.abs(1 - at(t)) > restDelta) lastOutside = t;\n    else if (t - lastOutside > 0.1) break;\n  }\n  return Math.max(lastOutside + step, step);\n}\n\n/**\n * A spring as a duration and an easing function over it — what GSAP takes:\n *\n *   const { duration, ease } = springEase({ stiffness: 300, damping: 20 });\n *   gsap.to(el, { y: 0, duration, ease });\n *\n * Exact for a fixed start and end; a spring that has to react mid-flight to a\n * new target needs `createSpringFollower` instead.\n */\nexport function springEase(options: SpringOptions = {}) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  return {\n    duration,\n    ease: (progress: number) => (progress >= 1 ? 1 : at(progress * duration)),\n  };\n}\n\n/**\n * A spring as a CSS `linear()` easing and its duration in ms, for\n * `transition` / `animation` with a fixed start and end.\n *\n *   const { easing, duration } = springLinear({ stiffness: 400, damping: 30 });\n *   el.style.transition = `transform ${duration}ms ${easing}`;\n */\nexport function springLinear(options: SpringOptions = {}, pointsPerSecond = 60) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  const count = Math.max(Math.ceil(duration * pointsPerSecond), 8);\n  const points: string[] = [];\n  for (let i = 0; i <= count; i++) {\n    const t = (i / count) * duration;\n    points.push(String(Math.round((i === count ? 1 : at(t)) * 10000) / 10000));\n  }\n  return { easing: `linear(${points.join(\", \")})`, duration: Math.round(duration * 1000) };\n}\n\n// ---------------------------------------------------------------------------\n// Spring follower (useSpring equivalent)\n// ---------------------------------------------------------------------------\n\n/** Something that calls back once per frame with the time since the last, in ms. */\nexport interface FrameTicker {\n  add: (callback: (deltaMs: number) => void) => () => void;\n}\n\n/** requestAnimationFrame as a ticker — the library-free default. */\nexport const rafTicker: FrameTicker = {\n  add(callback) {\n    let last = performance.now();\n    let frame = requestAnimationFrame(function tick(now) {\n      callback(now - last);\n      last = now;\n      frame = requestAnimationFrame(tick);\n    });\n    return () => cancelAnimationFrame(frame);\n  },\n};\n\nexport interface SpringFollower {\n  /** Chase a new target from wherever the value is now, keeping its velocity. */\n  set: (target: number) => void;\n  /** Move to a value instantly, with no motion. */\n  jump: (value: number) => void;\n  get: () => number;\n  destroy: () => void;\n}\n\n/**\n * A value that follows its target through a spring, integrating the same\n * equation of motion Motion's `useSpring` solves — so a pointer-driven spring\n * reads the same in every build. Runs only while moving; `onUpdate` receives\n * each new value.\n */\nexport function createSpringFollower(\n  initial: number,\n  onUpdate: (value: number) => void,\n  options: SpringOptions & { restSpeed?: number; ticker?: FrameTicker } = {},\n): SpringFollower {\n  const { stiffness = 100, damping = 10, mass = 1, ticker = rafTicker } = options;\n  const restDelta = options.restDelta ?? 0.01;\n  const restSpeed = options.restSpeed ?? 0.01;\n  let value = initial;\n  let target = initial;\n  let velocity = 0;\n  let stop: (() => void) | null = null;\n\n  const step = (deltaMs: number) => {\n    // Fixed 1ms sub-steps keep the integration stable for stiff springs and\n    // independent of the frame rate.\n    const steps = Math.min(Math.max(Math.round(deltaMs), 1), 250);\n    for (let i = 0; i < steps; i++) {\n      const force = -stiffness * (value - target) - damping * velocity;\n      velocity += (force / mass) * 0.001;\n      value += velocity * 0.001;\n    }\n    if (Math.abs(target - value) < restDelta && Math.abs(velocity) < restSpeed) {\n      value = target;\n      velocity = 0;\n      stop?.();\n      stop = null;\n    }\n    onUpdate(value);\n  };\n\n  return {\n    set(next) {\n      target = next;\n      if (!stop && value !== target) stop = ticker.add(step);\n    },\n    jump(next) {\n      stop?.();\n      stop = null;\n      value = target = next;\n      velocity = 0;\n      onUpdate(value);\n    },\n    get: () => value,\n    destroy() {\n      stop?.();\n      stop = null;\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/motion-curves.ts"
    }
  ],
  "docs": "Named export: SelectionFrame (also exports the FrameHandles and FrameLine types). Wrap any child; it is measured live with a ResizeObserver. `handles` is square | circle, `line` is solid | dashed | long, `dash` overrides the [dash, gap] pair, `animated` marches dashed/long strokes and pops the handles in, `color` is any CSS colour (Figma blue default), `radius` and `padding` are pixels, and `inline={false}` makes it block-level. Respects prefers-reduced-motion.",
  "categories": [
    "typography"
  ],
  "type": "registry:component"
}
